Skip to content

Commit a055a59

Browse files
authored
Changes in typed signature validation and normalization (#318)
1 parent 725b8c8 commit a055a59

File tree

4 files changed

+167
-63
lines changed

4 files changed

+167
-63
lines changed

src/utils/normalize.test.ts

Lines changed: 5 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ const MESSAGE_DATA_MOCK = {
4747
name: 'Liquid staked Ether 2.0',
4848
version: '2',
4949
chainId: '0x1',
50-
verifyingContract: '996101235222674412020337938588541139382869425796',
50+
verifyingContract: '0xae7ab96520de3a18e5e111b5eaab095312d7fe84',
5151
},
5252
primaryType: 'Permit',
5353
message: {
@@ -66,38 +66,16 @@ describe('normalizeTypedMessage', () => {
6666
}
6767

6868
it('should normalize verifyingContract address in domain', () => {
69-
const normalizedData = parseNormalizerResult(MESSAGE_DATA_MOCK);
70-
expect(normalizedData.domain.verifyingContract).toBe(
71-
'0xae7ab96520de3a18e5e111b5eaab095312d7fe84',
72-
);
73-
});
74-
75-
it('should normalize verifyingContract address in domain when provided data is an object', () => {
76-
const NON_STRINGIFIED_MESSAGE_DATA_MOCK = MESSAGE_DATA_MOCK;
77-
const normalizedData = JSON.parse(
78-
normalizeTypedMessage(
79-
NON_STRINGIFIED_MESSAGE_DATA_MOCK as unknown as string,
80-
),
81-
);
82-
expect(normalizedData.domain.verifyingContract).toBe(
83-
'0xae7ab96520de3a18e5e111b5eaab095312d7fe84',
84-
);
85-
});
86-
87-
it('should handle octal verifyingContract address by normalizing it', () => {
88-
const expectedNormalizedOctalAddress = '0x53';
89-
const messageDataWithOctalAddress = {
69+
const msgMock = {
9070
...MESSAGE_DATA_MOCK,
9171
domain: {
9272
...MESSAGE_DATA_MOCK.domain,
93-
verifyingContract: '0o123',
73+
verifyingContract: '0Xae7ab96520de3a18e5e111b5eaab095312d7fe84',
9474
},
9575
};
96-
97-
const normalizedData = parseNormalizerResult(messageDataWithOctalAddress);
98-
76+
const normalizedData = parseNormalizerResult(msgMock);
9977
expect(normalizedData.domain.verifyingContract).toBe(
100-
expectedNormalizedOctalAddress,
78+
'0xae7ab96520de3a18e5e111b5eaab095312d7fe84',
10179
);
10280
});
10381

src/utils/normalize.ts

Lines changed: 6 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
1-
import { add0x, isValidHexAddress, isStrictHexString } from '@metamask/utils';
21
import type { Hex } from '@metamask/utils';
3-
import BN from 'bn.js';
42

53
type EIP712Domain = {
6-
verifyingContract: string;
4+
verifyingContract: Hex;
75
};
86

97
type SignTypedMessageDataV3V4 = {
@@ -45,7 +43,7 @@ export function normalizeTypedMessage(messageData: string) {
4543
* @param data - The messageData to parse.
4644
* @returns The data object for EIP712 normalization.
4745
*/
48-
function parseTypedMessage(data: string) {
46+
export function parseTypedMessage(data: string) {
4947
if (typeof data !== 'string') {
5048
return data;
5149
}
@@ -54,36 +52,14 @@ function parseTypedMessage(data: string) {
5452
}
5553

5654
/**
57-
* Normalizes the address to a hexadecimal format
55+
* Normalizes the address to standard hexadecimal format
5856
*
5957
* @param address - The address to normalize.
6058
* @returns The normalized address.
6159
*/
62-
function normalizeContractAddress(address: string): Hex | string {
63-
if (isStrictHexString(address) && isValidHexAddress(address)) {
64-
return address;
60+
function normalizeContractAddress(address: Hex): Hex {
61+
if (address.startsWith('0X')) {
62+
return `0x${address.slice(2)}`;
6563
}
66-
67-
// Check if the address is in octal format, convert to hexadecimal
68-
if (address.startsWith('0o')) {
69-
// If octal, convert to hexadecimal
70-
return octalToHex(address);
71-
}
72-
73-
// Check if the address is in decimal format, convert to hexadecimal
74-
try {
75-
const decimalBN = new BN(address, 10);
76-
const hexString = decimalBN.toString(16);
77-
return add0x(hexString);
78-
} catch (e) {
79-
// Ignore errors and return the original address
80-
}
81-
82-
// Returning the original address without normalization
8364
return address;
8465
}
85-
86-
function octalToHex(octalAddress: string): Hex {
87-
const decimalAddress = parseInt(octalAddress.slice(2), 8).toString(16);
88-
return add0x(decimalAddress);
89-
}

src/wallet.test.ts

Lines changed: 134 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -357,7 +357,7 @@ describe('wallet', () => {
357357
},
358358
primaryType: 'EIP712Domain',
359359
domain: {
360-
verifyingContract: '996101235222674412020337938588541139382869425796',
360+
verifyingContract: '0Xae7ab96520de3a18e5e111b5eaab095312d7fe84',
361361
},
362362
message: {},
363363
};
@@ -391,6 +391,139 @@ describe('wallet', () => {
391391
signatureMethod: 'eth_signTypedData_v3',
392392
});
393393
});
394+
395+
it('should throw if verifyingContract is invalid hex value', async () => {
396+
const { engine } = createTestSetup();
397+
const getAccounts = async () => testAddresses.slice();
398+
const witnessedMsgParams: TypedMessageParams[] = [];
399+
const processTypedMessageV3 = async (msgParams: TypedMessageParams) => {
400+
witnessedMsgParams.push(msgParams);
401+
// Assume testMsgSig is the expected signature result
402+
return testMsgSig;
403+
};
404+
405+
engine.push(
406+
createWalletMiddleware({ getAccounts, processTypedMessageV3 }),
407+
);
408+
409+
const message = {
410+
types: {
411+
EIP712Domain: [
412+
{ name: 'name', type: 'string' },
413+
{ name: 'version', type: 'string' },
414+
{ name: 'chainId', type: 'uint256' },
415+
{ name: 'verifyingContract', type: 'address' },
416+
],
417+
},
418+
primaryType: 'EIP712Domain',
419+
domain: {
420+
verifyingContract: '917551056842671309452305380979543736893630245704',
421+
},
422+
message: {},
423+
};
424+
425+
const stringifiedMessage = JSON.stringify(message);
426+
427+
const payload = {
428+
method: 'eth_signTypedData_v3',
429+
params: [testAddresses[0], stringifiedMessage], // Assuming testAddresses[0] is a valid address from your setup
430+
};
431+
432+
const promise = pify(engine.handle).call(engine, payload);
433+
await expect(promise).rejects.toThrow('Invalid input.');
434+
});
435+
});
436+
437+
describe('signTypedDataV4', () => {
438+
const getMsgParams = (verifyingContract?: string) => ({
439+
types: {
440+
EIP712Domain: [
441+
{ name: 'name', type: 'string' },
442+
{ name: 'version', type: 'string' },
443+
{ name: 'chainId', type: 'uint256' },
444+
{ name: 'verifyingContract', type: 'address' },
445+
],
446+
Permit: [
447+
{ name: 'owner', type: 'address' },
448+
{ name: 'spender', type: 'address' },
449+
{ name: 'value', type: 'uint256' },
450+
{ name: 'nonce', type: 'uint256' },
451+
{ name: 'deadline', type: 'uint256' },
452+
],
453+
},
454+
primaryType: 'Permit',
455+
domain: {
456+
name: 'MyToken',
457+
version: '1',
458+
verifyingContract:
459+
verifyingContract ?? '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC',
460+
chainId: '0x1',
461+
},
462+
message: {
463+
owner: testAddresses[0],
464+
spender: '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc',
465+
value: 3000,
466+
nonce: 0,
467+
deadline: 50000000000,
468+
},
469+
});
470+
471+
it('should not throw if request is permit with valid hex value for verifyingContract address', async () => {
472+
const { engine } = createTestSetup();
473+
const getAccounts = async () => testAddresses.slice();
474+
const witnessedMsgParams: TypedMessageParams[] = [];
475+
const processTypedMessageV4 = async (msgParams: TypedMessageParams) => {
476+
witnessedMsgParams.push(msgParams);
477+
// Assume testMsgSig is the expected signature result
478+
return testMsgSig;
479+
};
480+
481+
engine.push(
482+
createWalletMiddleware({ getAccounts, processTypedMessageV4 }),
483+
);
484+
485+
const payload = {
486+
method: 'eth_signTypedData_v4',
487+
params: [testAddresses[0], JSON.stringify(getMsgParams())],
488+
};
489+
490+
const promise = pify(engine.handle).call(engine, payload);
491+
const result = await promise;
492+
expect(result).toStrictEqual({
493+
id: undefined,
494+
jsonrpc: undefined,
495+
result:
496+
'0x68dc980608bceb5f99f691e62c32caccaee05317309015e9454eba1a14c3cd4505d1dd098b8339801239c9bcaac3c4df95569dcf307108b92f68711379be14d81c',
497+
});
498+
});
499+
500+
it('should throw if request is permit with invalid hex value for verifyingContract address', async () => {
501+
const { engine } = createTestSetup();
502+
const getAccounts = async () => testAddresses.slice();
503+
const witnessedMsgParams: TypedMessageParams[] = [];
504+
const processTypedMessageV4 = async (msgParams: TypedMessageParams) => {
505+
witnessedMsgParams.push(msgParams);
506+
// Assume testMsgSig is the expected signature result
507+
return testMsgSig;
508+
};
509+
510+
engine.push(
511+
createWalletMiddleware({ getAccounts, processTypedMessageV4 }),
512+
);
513+
514+
const payload = {
515+
method: 'eth_signTypedData_v4',
516+
params: [
517+
testAddresses[0],
518+
JSON.stringify(
519+
getMsgParams('917551056842671309452305380979543736893630245704'),
520+
),
521+
],
522+
};
523+
524+
const promise = pify(engine.handle).call(engine, payload);
525+
await expect(promise).rejects.toThrow('Invalid input.');
526+
});
394527
});
395528

396529
describe('sign', () => {

src/wallet.ts

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,15 @@ import {
55
createScaffoldMiddleware,
66
} from '@metamask/json-rpc-engine';
77
import { providerErrors, rpcErrors } from '@metamask/rpc-errors';
8-
import type {
9-
Json,
10-
JsonRpcRequest,
11-
PendingJsonRpcResponse,
8+
import {
9+
isValidHexAddress,
10+
type Json,
11+
type JsonRpcRequest,
12+
type PendingJsonRpcResponse,
1213
} from '@metamask/utils';
1314

1415
import type { Block } from './types';
15-
import { normalizeTypedMessage } from './utils/normalize';
16+
import { normalizeTypedMessage, parseTypedMessage } from './utils/normalize';
1617

1718
/*
1819
export type TransactionParams = {
@@ -278,6 +279,7 @@ WalletMiddlewareOptions): JsonRpcMiddleware<any, Block> {
278279

279280
const address = await validateAndNormalizeKeyholder(params[0], req);
280281
const message = normalizeTypedMessage(params[1]);
282+
validateVerifyingContract(message);
281283
const version = 'V3';
282284
const msgParams: TypedMessageParams = {
283285
data: message,
@@ -308,6 +310,7 @@ WalletMiddlewareOptions): JsonRpcMiddleware<any, Block> {
308310

309311
const address = await validateAndNormalizeKeyholder(params[0], req);
310312
const message = normalizeTypedMessage(params[1]);
313+
validateVerifyingContract(message);
311314
const version = 'V4';
312315
const msgParams: TypedMessageParams = {
313316
data: message,
@@ -490,6 +493,20 @@ WalletMiddlewareOptions): JsonRpcMiddleware<any, Block> {
490493
}
491494
}
492495

496+
/**
497+
* Validates verifyingContract of typedSignMessage.
498+
*
499+
* @param data - The data passed in typedSign request.
500+
*/
501+
function validateVerifyingContract(data: string) {
502+
const {
503+
domain: { verifyingContract },
504+
} = parseTypedMessage(data);
505+
if (!isValidHexAddress(verifyingContract)) {
506+
throw rpcErrors.invalidInput();
507+
}
508+
}
509+
493510
function resemblesAddress(str: string): boolean {
494511
// hex prefix 2 + 20 bytes
495512
return str.length === 2 + 20 * 2;

0 commit comments

Comments
 (0)