Skip to content

Commit

Permalink
Changes in typed signature validation and normalization (#318)
Browse files Browse the repository at this point in the history
  • Loading branch information
jpuri authored Jul 23, 2024
1 parent 725b8c8 commit a055a59
Show file tree
Hide file tree
Showing 4 changed files with 167 additions and 63 deletions.
32 changes: 5 additions & 27 deletions src/utils/normalize.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ const MESSAGE_DATA_MOCK = {
name: 'Liquid staked Ether 2.0',
version: '2',
chainId: '0x1',
verifyingContract: '996101235222674412020337938588541139382869425796',
verifyingContract: '0xae7ab96520de3a18e5e111b5eaab095312d7fe84',
},
primaryType: 'Permit',
message: {
Expand All @@ -66,38 +66,16 @@ describe('normalizeTypedMessage', () => {
}

it('should normalize verifyingContract address in domain', () => {
const normalizedData = parseNormalizerResult(MESSAGE_DATA_MOCK);
expect(normalizedData.domain.verifyingContract).toBe(
'0xae7ab96520de3a18e5e111b5eaab095312d7fe84',
);
});

it('should normalize verifyingContract address in domain when provided data is an object', () => {
const NON_STRINGIFIED_MESSAGE_DATA_MOCK = MESSAGE_DATA_MOCK;
const normalizedData = JSON.parse(
normalizeTypedMessage(
NON_STRINGIFIED_MESSAGE_DATA_MOCK as unknown as string,
),
);
expect(normalizedData.domain.verifyingContract).toBe(
'0xae7ab96520de3a18e5e111b5eaab095312d7fe84',
);
});

it('should handle octal verifyingContract address by normalizing it', () => {
const expectedNormalizedOctalAddress = '0x53';
const messageDataWithOctalAddress = {
const msgMock = {
...MESSAGE_DATA_MOCK,
domain: {
...MESSAGE_DATA_MOCK.domain,
verifyingContract: '0o123',
verifyingContract: '0Xae7ab96520de3a18e5e111b5eaab095312d7fe84',
},
};

const normalizedData = parseNormalizerResult(messageDataWithOctalAddress);

const normalizedData = parseNormalizerResult(msgMock);
expect(normalizedData.domain.verifyingContract).toBe(
expectedNormalizedOctalAddress,
'0xae7ab96520de3a18e5e111b5eaab095312d7fe84',
);
});

Expand Down
36 changes: 6 additions & 30 deletions src/utils/normalize.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import { add0x, isValidHexAddress, isStrictHexString } from '@metamask/utils';
import type { Hex } from '@metamask/utils';
import BN from 'bn.js';

type EIP712Domain = {
verifyingContract: string;
verifyingContract: Hex;
};

type SignTypedMessageDataV3V4 = {
Expand Down Expand Up @@ -45,7 +43,7 @@ export function normalizeTypedMessage(messageData: string) {
* @param data - The messageData to parse.
* @returns The data object for EIP712 normalization.
*/
function parseTypedMessage(data: string) {
export function parseTypedMessage(data: string) {
if (typeof data !== 'string') {
return data;
}
Expand All @@ -54,36 +52,14 @@ function parseTypedMessage(data: string) {
}

/**
* Normalizes the address to a hexadecimal format
* Normalizes the address to standard hexadecimal format

Check warning on line 55 in src/utils/normalize.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test / Lint (18.x)

JSDoc description does not satisfy the regex pattern

Check warning on line 55 in src/utils/normalize.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test / Lint (20.x)

JSDoc description does not satisfy the regex pattern

Check warning on line 55 in src/utils/normalize.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test / Lint (22.x)

JSDoc description does not satisfy the regex pattern
*
* @param address - The address to normalize.
* @returns The normalized address.
*/
function normalizeContractAddress(address: string): Hex | string {
if (isStrictHexString(address) && isValidHexAddress(address)) {
return address;
function normalizeContractAddress(address: Hex): Hex {
if (address.startsWith('0X')) {
return `0x${address.slice(2)}`;
}

// Check if the address is in octal format, convert to hexadecimal
if (address.startsWith('0o')) {
// If octal, convert to hexadecimal
return octalToHex(address);
}

// Check if the address is in decimal format, convert to hexadecimal
try {
const decimalBN = new BN(address, 10);
const hexString = decimalBN.toString(16);
return add0x(hexString);
} catch (e) {
// Ignore errors and return the original address
}

// Returning the original address without normalization
return address;
}

function octalToHex(octalAddress: string): Hex {
const decimalAddress = parseInt(octalAddress.slice(2), 8).toString(16);
return add0x(decimalAddress);
}
135 changes: 134 additions & 1 deletion src/wallet.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -357,7 +357,7 @@ describe('wallet', () => {
},
primaryType: 'EIP712Domain',
domain: {
verifyingContract: '996101235222674412020337938588541139382869425796',
verifyingContract: '0Xae7ab96520de3a18e5e111b5eaab095312d7fe84',
},
message: {},
};
Expand Down Expand Up @@ -391,6 +391,139 @@ describe('wallet', () => {
signatureMethod: 'eth_signTypedData_v3',
});
});

it('should throw if verifyingContract is invalid hex value', async () => {
const { engine } = createTestSetup();
const getAccounts = async () => testAddresses.slice();
const witnessedMsgParams: TypedMessageParams[] = [];
const processTypedMessageV3 = async (msgParams: TypedMessageParams) => {
witnessedMsgParams.push(msgParams);
// Assume testMsgSig is the expected signature result
return testMsgSig;
};

engine.push(
createWalletMiddleware({ getAccounts, processTypedMessageV3 }),
);

const message = {
types: {
EIP712Domain: [
{ name: 'name', type: 'string' },
{ name: 'version', type: 'string' },
{ name: 'chainId', type: 'uint256' },
{ name: 'verifyingContract', type: 'address' },
],
},
primaryType: 'EIP712Domain',
domain: {
verifyingContract: '917551056842671309452305380979543736893630245704',
},
message: {},
};

const stringifiedMessage = JSON.stringify(message);

const payload = {
method: 'eth_signTypedData_v3',
params: [testAddresses[0], stringifiedMessage], // Assuming testAddresses[0] is a valid address from your setup
};

const promise = pify(engine.handle).call(engine, payload);
await expect(promise).rejects.toThrow('Invalid input.');
});
});

describe('signTypedDataV4', () => {
const getMsgParams = (verifyingContract?: string) => ({
types: {
EIP712Domain: [
{ name: 'name', type: 'string' },
{ name: 'version', type: 'string' },
{ name: 'chainId', type: 'uint256' },
{ name: 'verifyingContract', type: 'address' },
],
Permit: [
{ name: 'owner', type: 'address' },
{ name: 'spender', type: 'address' },
{ name: 'value', type: 'uint256' },
{ name: 'nonce', type: 'uint256' },
{ name: 'deadline', type: 'uint256' },
],
},
primaryType: 'Permit',
domain: {
name: 'MyToken',
version: '1',
verifyingContract:
verifyingContract ?? '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC',
chainId: '0x1',
},
message: {
owner: testAddresses[0],
spender: '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc',
value: 3000,
nonce: 0,
deadline: 50000000000,
},
});

it('should not throw if request is permit with valid hex value for verifyingContract address', async () => {
const { engine } = createTestSetup();
const getAccounts = async () => testAddresses.slice();
const witnessedMsgParams: TypedMessageParams[] = [];
const processTypedMessageV4 = async (msgParams: TypedMessageParams) => {
witnessedMsgParams.push(msgParams);
// Assume testMsgSig is the expected signature result
return testMsgSig;
};

engine.push(
createWalletMiddleware({ getAccounts, processTypedMessageV4 }),
);

const payload = {
method: 'eth_signTypedData_v4',
params: [testAddresses[0], JSON.stringify(getMsgParams())],
};

const promise = pify(engine.handle).call(engine, payload);
const result = await promise;
expect(result).toStrictEqual({
id: undefined,
jsonrpc: undefined,
result:
'0x68dc980608bceb5f99f691e62c32caccaee05317309015e9454eba1a14c3cd4505d1dd098b8339801239c9bcaac3c4df95569dcf307108b92f68711379be14d81c',
});
});

it('should throw if request is permit with invalid hex value for verifyingContract address', async () => {
const { engine } = createTestSetup();
const getAccounts = async () => testAddresses.slice();
const witnessedMsgParams: TypedMessageParams[] = [];
const processTypedMessageV4 = async (msgParams: TypedMessageParams) => {
witnessedMsgParams.push(msgParams);
// Assume testMsgSig is the expected signature result
return testMsgSig;
};

engine.push(
createWalletMiddleware({ getAccounts, processTypedMessageV4 }),
);

const payload = {
method: 'eth_signTypedData_v4',
params: [
testAddresses[0],
JSON.stringify(
getMsgParams('917551056842671309452305380979543736893630245704'),
),
],
};

const promise = pify(engine.handle).call(engine, payload);
await expect(promise).rejects.toThrow('Invalid input.');
});
});

describe('sign', () => {
Expand Down
27 changes: 22 additions & 5 deletions src/wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@ import {
createScaffoldMiddleware,
} from '@metamask/json-rpc-engine';
import { providerErrors, rpcErrors } from '@metamask/rpc-errors';
import type {
Json,
JsonRpcRequest,
PendingJsonRpcResponse,
import {
isValidHexAddress,
type Json,
type JsonRpcRequest,
type PendingJsonRpcResponse,
} from '@metamask/utils';

import type { Block } from './types';
import { normalizeTypedMessage } from './utils/normalize';
import { normalizeTypedMessage, parseTypedMessage } from './utils/normalize';

/*
export type TransactionParams = {
Expand Down Expand Up @@ -278,6 +279,7 @@ WalletMiddlewareOptions): JsonRpcMiddleware<any, Block> {

const address = await validateAndNormalizeKeyholder(params[0], req);
const message = normalizeTypedMessage(params[1]);
validateVerifyingContract(message);
const version = 'V3';
const msgParams: TypedMessageParams = {
data: message,
Expand Down Expand Up @@ -308,6 +310,7 @@ WalletMiddlewareOptions): JsonRpcMiddleware<any, Block> {

const address = await validateAndNormalizeKeyholder(params[0], req);
const message = normalizeTypedMessage(params[1]);
validateVerifyingContract(message);
const version = 'V4';
const msgParams: TypedMessageParams = {
data: message,
Expand Down Expand Up @@ -490,6 +493,20 @@ WalletMiddlewareOptions): JsonRpcMiddleware<any, Block> {
}
}

/**
* Validates verifyingContract of typedSignMessage.
*
* @param data - The data passed in typedSign request.
*/
function validateVerifyingContract(data: string) {
const {
domain: { verifyingContract },
} = parseTypedMessage(data);
if (!isValidHexAddress(verifyingContract)) {
throw rpcErrors.invalidInput();
}
}

function resemblesAddress(str: string): boolean {
// hex prefix 2 + 20 bytes
return str.length === 2 + 20 * 2;
Expand Down

0 comments on commit a055a59

Please sign in to comment.