Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Changes in typed signature validation and normalization #318

Merged
merged 8 commits into from
Jul 23, 2024
Merged
Show file tree
Hide file tree
Changes from 3 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
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 All @@ -14,7 +12,7 @@
};

/**
* Normalizes the messageData for the eth_signTypedData

Check warning on line 15 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 15 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
*
* @param messageData - The messageData to normalize.
* @returns The normalized messageData.
Expand All @@ -40,12 +38,12 @@
}

/**
* Parses the messageData to obtain the data object for EIP712 normalization

Check warning on line 41 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 41 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
*
* @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 @@
}

/**
* 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
*
* @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)}`;
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Normilizing decimal and octal value is removed now, only normalization now is replacing 0X prefix with 0x.


// 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 @@
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 @@

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 @@

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 @@ -459,7 +462,7 @@
*
* @param address - The address to validate and normalize.
* @param req - The request object.
* @returns {string} - The normalized address, if valid. Otherwise, throws

Check warning on line 465 in src/wallet.ts

View workflow job for this annotation

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

There must be no hyphen before @returns description

Check warning on line 465 in src/wallet.ts

View workflow job for this annotation

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

There must be no hyphen before @returns description
* an error
*/
async function validateAndNormalizeKeyholder(
Expand Down Expand Up @@ -490,6 +493,20 @@
}
}

/**
* 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
Loading