389 lines
18 KiB
TypeScript
389 lines
18 KiB
TypeScript
import { schemas } from '@0xproject/json-schemas';
|
|
import { ECSignature, SignatureType, SignerType, ValidatorSignature } from '@0xproject/types';
|
|
import { Web3Wrapper } from '@0xproject/web3-wrapper';
|
|
import { Provider } from 'ethereum-types';
|
|
import * as ethUtil from 'ethereumjs-util';
|
|
import * as _ from 'lodash';
|
|
|
|
import { artifacts } from './artifacts';
|
|
import { assert } from './assert';
|
|
import { ExchangeContract } from './generated_contract_wrappers/exchange';
|
|
import { IValidatorContract } from './generated_contract_wrappers/i_validator';
|
|
import { IWalletContract } from './generated_contract_wrappers/i_wallet';
|
|
import { OrderError } from './types';
|
|
import { utils } from './utils';
|
|
|
|
export const signatureUtils = {
|
|
/**
|
|
* Verifies that the provided signature is valid according to the 0x Protocol smart contracts
|
|
* @param data The hex encoded data signed by the supplied signature.
|
|
* @param signature A hex encoded 0x Protocol signature made up of: [TypeSpecificData][SignatureType].
|
|
* E.g [vrs][SignatureType.EIP712]
|
|
* @param signerAddress The hex encoded address that signed the data, producing the supplied signature.
|
|
* @return Whether the signature is valid for the supplied signerAddress and data.
|
|
*/
|
|
async isValidSignatureAsync(
|
|
provider: Provider,
|
|
data: string,
|
|
signature: string,
|
|
signerAddress: string,
|
|
): Promise<boolean> {
|
|
assert.isWeb3Provider('provider', provider);
|
|
assert.isHexString('data', data);
|
|
assert.isHexString('signature', signature);
|
|
assert.isETHAddressHex('signerAddress', signerAddress);
|
|
const signatureTypeIndexIfExists = utils.getSignatureTypeIndexIfExists(signature);
|
|
if (_.isUndefined(signatureTypeIndexIfExists)) {
|
|
throw new Error(`Unrecognized signatureType in signature: ${signature}`);
|
|
}
|
|
|
|
switch (signatureTypeIndexIfExists) {
|
|
case SignatureType.Illegal:
|
|
case SignatureType.Invalid:
|
|
return false;
|
|
|
|
case SignatureType.EIP712: {
|
|
const ecSignature = signatureUtils.parseECSignature(signature);
|
|
return signatureUtils.isValidECSignature(data, ecSignature, signerAddress);
|
|
}
|
|
|
|
case SignatureType.EthSign: {
|
|
const ecSignature = signatureUtils.parseECSignature(signature);
|
|
const prefixedMessageHex = signatureUtils.addSignedMessagePrefix(data, SignerType.Default);
|
|
return signatureUtils.isValidECSignature(prefixedMessageHex, ecSignature, signerAddress);
|
|
}
|
|
|
|
case SignatureType.Wallet: {
|
|
const isValid = await signatureUtils.isValidWalletSignatureAsync(
|
|
provider,
|
|
data,
|
|
signature,
|
|
signerAddress,
|
|
);
|
|
return isValid;
|
|
}
|
|
|
|
case SignatureType.Validator: {
|
|
const isValid = await signatureUtils.isValidValidatorSignatureAsync(
|
|
provider,
|
|
data,
|
|
signature,
|
|
signerAddress,
|
|
);
|
|
return isValid;
|
|
}
|
|
|
|
case SignatureType.PreSigned: {
|
|
return signatureUtils.isValidPresignedSignatureAsync(provider, data, signerAddress);
|
|
}
|
|
|
|
default:
|
|
throw new Error(`Unhandled SignatureType: ${signatureTypeIndexIfExists}`);
|
|
}
|
|
},
|
|
/**
|
|
* Verifies that the provided presigned signature is valid according to the 0x Protocol smart contracts
|
|
* @param provider Web3 provider to use for all JSON RPC requests
|
|
* @param data The hex encoded data signed by the supplied signature
|
|
* @param signerAddress The hex encoded address that signed the data, producing the supplied signature.
|
|
* @return Whether the data was preSigned by the supplied signerAddress
|
|
*/
|
|
async isValidPresignedSignatureAsync(provider: Provider, data: string, signerAddress: string): Promise<boolean> {
|
|
assert.isWeb3Provider('provider', provider);
|
|
assert.isHexString('data', data);
|
|
assert.isETHAddressHex('signerAddress', signerAddress);
|
|
const exchangeContract = new ExchangeContract(artifacts.Exchange.compilerOutput.abi, signerAddress, provider);
|
|
const isValid = await exchangeContract.preSigned.callAsync(data, signerAddress);
|
|
return isValid;
|
|
},
|
|
/**
|
|
* Verifies that the provided wallet signature is valid according to the 0x Protocol smart contracts
|
|
* @param provider Web3 provider to use for all JSON RPC requests
|
|
* @param data The hex encoded data signed by the supplied signature.
|
|
* @param signature A hex encoded presigned 0x Protocol signature made up of: [SignatureType.Presigned]
|
|
* @param signerAddress The hex encoded address that signed the data, producing the supplied signature.
|
|
* @return Whether the data was preSigned by the supplied signerAddress.
|
|
*/
|
|
async isValidWalletSignatureAsync(
|
|
provider: Provider,
|
|
data: string,
|
|
signature: string,
|
|
signerAddress: string,
|
|
): Promise<boolean> {
|
|
assert.isWeb3Provider('provider', provider);
|
|
assert.isHexString('data', data);
|
|
assert.isHexString('signature', signature);
|
|
assert.isETHAddressHex('signerAddress', signerAddress);
|
|
// tslint:disable-next-line:custom-no-magic-numbers
|
|
const signatureWithoutType = signature.slice(-2);
|
|
const walletContract = new IWalletContract(artifacts.IWallet.compilerOutput.abi, signerAddress, provider);
|
|
const isValid = await walletContract.isValidSignature.callAsync(data, signatureWithoutType);
|
|
return isValid;
|
|
},
|
|
/**
|
|
* Verifies that the provided validator signature is valid according to the 0x Protocol smart contracts
|
|
* @param provider Web3 provider to use for all JSON RPC requests
|
|
* @param data The hex encoded data signed by the supplied signature.
|
|
* @param signature A hex encoded presigned 0x Protocol signature made up of: [SignatureType.Presigned]
|
|
* @param signerAddress The hex encoded address that signed the data, producing the supplied signature.
|
|
* @return Whether the data was preSigned by the supplied signerAddress.
|
|
*/
|
|
async isValidValidatorSignatureAsync(
|
|
provider: Provider,
|
|
data: string,
|
|
signature: string,
|
|
signerAddress: string,
|
|
): Promise<boolean> {
|
|
assert.isWeb3Provider('provider', provider);
|
|
assert.isHexString('data', data);
|
|
assert.isHexString('signature', signature);
|
|
assert.isETHAddressHex('signerAddress', signerAddress);
|
|
const validatorSignature = parseValidatorSignature(signature);
|
|
const exchangeContract = new ExchangeContract(artifacts.Exchange.compilerOutput.abi, signerAddress, provider);
|
|
const isValidatorApproved = await exchangeContract.allowedValidators.callAsync(
|
|
signerAddress,
|
|
validatorSignature.validatorAddress,
|
|
);
|
|
if (!isValidatorApproved) {
|
|
throw new Error(
|
|
`Validator ${validatorSignature.validatorAddress} was not pre-approved by ${signerAddress}.`,
|
|
);
|
|
}
|
|
|
|
const validatorContract = new IValidatorContract(
|
|
artifacts.IValidator.compilerOutput.abi,
|
|
signerAddress,
|
|
provider,
|
|
);
|
|
const isValid = await validatorContract.isValidSignature.callAsync(
|
|
data,
|
|
signerAddress,
|
|
validatorSignature.signature,
|
|
);
|
|
return isValid;
|
|
},
|
|
/**
|
|
* Checks if the supplied elliptic curve signature corresponds to signing `data` with
|
|
* the private key corresponding to `signerAddress`
|
|
* @param data The hex encoded data signed by the supplied signature.
|
|
* @param signature An object containing the elliptic curve signature parameters.
|
|
* @param signerAddress The hex encoded address that signed the data, producing the supplied signature.
|
|
* @return Whether the ECSignature is valid.
|
|
*/
|
|
isValidECSignature(data: string, signature: ECSignature, signerAddress: string): boolean {
|
|
assert.isHexString('data', data);
|
|
assert.doesConformToSchema('signature', signature, schemas.ecSignatureSchema);
|
|
assert.isETHAddressHex('signerAddress', signerAddress);
|
|
const normalizedSignerAddress = signerAddress.toLowerCase();
|
|
|
|
const msgHashBuff = ethUtil.toBuffer(data);
|
|
try {
|
|
const pubKey = ethUtil.ecrecover(
|
|
msgHashBuff,
|
|
signature.v,
|
|
ethUtil.toBuffer(signature.r),
|
|
ethUtil.toBuffer(signature.s),
|
|
);
|
|
const retrievedAddress = ethUtil.bufferToHex(ethUtil.pubToAddress(pubKey));
|
|
const normalizedRetrievedAddress = retrievedAddress.toLowerCase();
|
|
return normalizedRetrievedAddress === normalizedSignerAddress;
|
|
} catch (err) {
|
|
return false;
|
|
}
|
|
},
|
|
/**
|
|
* Signs an orderHash and returns it's elliptic curve signature and signature type.
|
|
* This method currently supports TestRPC, Geth and Parity above and below V1.6.6
|
|
* @param orderHash Hex encoded orderHash to sign.
|
|
* @param signerAddress The hex encoded Ethereum address you wish to sign it with. This address
|
|
* must be available via the Provider supplied to 0x.js.
|
|
* @param signerType Different signers add/require different prefixes to be prepended to the message being signed.
|
|
* Since we cannot know ahead of time which signer you are using, you must supply a SignerType.
|
|
* @return A hex encoded string containing the Elliptic curve signature generated by signing the orderHash and the Signature Type.
|
|
*/
|
|
async ecSignOrderHashAsync(
|
|
provider: Provider,
|
|
orderHash: string,
|
|
signerAddress: string,
|
|
signerType: SignerType,
|
|
): Promise<string> {
|
|
assert.isWeb3Provider('provider', provider);
|
|
assert.isHexString('orderHash', orderHash);
|
|
assert.isETHAddressHex('signerAddress', signerAddress);
|
|
const web3Wrapper = new Web3Wrapper(provider);
|
|
await assert.isSenderAddressAsync('signerAddress', signerAddress, web3Wrapper);
|
|
const normalizedSignerAddress = signerAddress.toLowerCase();
|
|
|
|
let msgHashHex = orderHash;
|
|
const prefixedMsgHashHex = signatureUtils.addSignedMessagePrefix(orderHash, signerType);
|
|
// Metamask incorrectly implements eth_sign and does not prefix the message as per the spec
|
|
// Source: https://github.com/MetaMask/metamask-extension/commit/a9d36860bec424dcee8db043d3e7da6a5ff5672e
|
|
if (signerType === SignerType.Metamask) {
|
|
msgHashHex = prefixedMsgHashHex;
|
|
}
|
|
const signature = await web3Wrapper.signMessageAsync(normalizedSignerAddress, msgHashHex);
|
|
|
|
// HACK: There is no consensus on whether the signatureHex string should be formatted as
|
|
// v + r + s OR r + s + v, and different clients (even different versions of the same client)
|
|
// return the signature params in different orders. In order to support all client implementations,
|
|
// we parse the signature in both ways, and evaluate if either one is a valid signature.
|
|
// r + s + v is the most prevalent format from eth_sign, so we attempt this first.
|
|
// tslint:disable-next-line:custom-no-magic-numbers
|
|
const validVParamValues = [27, 28];
|
|
const ecSignatureRSV = parseSignatureHexAsRSV(signature);
|
|
if (_.includes(validVParamValues, ecSignatureRSV.v)) {
|
|
const isValidRSVSignature = signatureUtils.isValidECSignature(
|
|
prefixedMsgHashHex,
|
|
ecSignatureRSV,
|
|
normalizedSignerAddress,
|
|
);
|
|
if (isValidRSVSignature) {
|
|
const convertedSignatureHex = signatureUtils.convertECSignatureToSignatureHex(
|
|
ecSignatureRSV,
|
|
signerType,
|
|
);
|
|
return convertedSignatureHex;
|
|
}
|
|
}
|
|
const ecSignatureVRS = parseSignatureHexAsVRS(signature);
|
|
if (_.includes(validVParamValues, ecSignatureVRS.v)) {
|
|
const isValidVRSSignature = signatureUtils.isValidECSignature(
|
|
prefixedMsgHashHex,
|
|
ecSignatureVRS,
|
|
normalizedSignerAddress,
|
|
);
|
|
if (isValidVRSSignature) {
|
|
const convertedSignatureHex = signatureUtils.convertECSignatureToSignatureHex(
|
|
ecSignatureVRS,
|
|
signerType,
|
|
);
|
|
return convertedSignatureHex;
|
|
}
|
|
}
|
|
|
|
throw new Error(OrderError.InvalidSignature);
|
|
},
|
|
/**
|
|
* Combines ECSignature with V,R,S and the relevant signature type for use in 0x protocol
|
|
* @param ecSignature The ECSignature of the signed data
|
|
* @param signerType The SignerType of the signed data
|
|
* @return Hex encoded string of signature (v,r,s) with Signature Type
|
|
*/
|
|
convertECSignatureToSignatureHex(ecSignature: ECSignature, signerType: SignerType): string {
|
|
const signatureBuffer = Buffer.concat([
|
|
ethUtil.toBuffer(ecSignature.v),
|
|
ethUtil.toBuffer(ecSignature.r),
|
|
ethUtil.toBuffer(ecSignature.s),
|
|
]);
|
|
const signatureHex = `0x${signatureBuffer.toString('hex')}`;
|
|
let signatureType;
|
|
switch (signerType) {
|
|
case SignerType.Metamask:
|
|
case SignerType.Ledger:
|
|
case SignerType.Default: {
|
|
signatureType = SignatureType.EthSign;
|
|
break;
|
|
}
|
|
default:
|
|
throw new Error(`Unrecognized SignerType: ${signerType}`);
|
|
}
|
|
const signatureWithType = signatureUtils.convertToSignatureWithType(signatureHex, signatureType);
|
|
return signatureWithType;
|
|
},
|
|
/**
|
|
* Combines the signature proof and the Signature Type.
|
|
* @param signature The hex encoded signature proof
|
|
* @param signatureType The signature type, i.e EthSign, Wallet etc.
|
|
* @return Hex encoded string of signature proof with Signature Type
|
|
*/
|
|
convertToSignatureWithType(signature: string, signatureType: SignatureType): string {
|
|
const signatureBuffer = Buffer.concat([ethUtil.toBuffer(signature), ethUtil.toBuffer(signatureType)]);
|
|
const signatureHex = `0x${signatureBuffer.toString('hex')}`;
|
|
return signatureHex;
|
|
},
|
|
/**
|
|
* Adds the relevant prefix to the message being signed.
|
|
* @param message Message to sign
|
|
* @param signerType The type of message prefix to add for a given SignerType. Different signers expect
|
|
* specific message prefixes.
|
|
* @return Prefixed message
|
|
*/
|
|
addSignedMessagePrefix(message: string, signerType: SignerType = SignerType.Default): string {
|
|
assert.isString('message', message);
|
|
assert.doesBelongToStringEnum('signerType', signerType, SignerType);
|
|
switch (signerType) {
|
|
case SignerType.Metamask:
|
|
case SignerType.Ledger:
|
|
case SignerType.Default: {
|
|
const msgBuff = ethUtil.toBuffer(message);
|
|
const prefixedMsgBuff = ethUtil.hashPersonalMessage(msgBuff);
|
|
const prefixedMsgHex = ethUtil.bufferToHex(prefixedMsgBuff);
|
|
return prefixedMsgHex;
|
|
}
|
|
default:
|
|
throw new Error(`Unrecognized SignerType: ${signerType}`);
|
|
}
|
|
},
|
|
/**
|
|
* Parse a 0x protocol hex-encoded signature string into it's ECSignature components
|
|
* @param signature A hex encoded ecSignature 0x Protocol signature
|
|
* @return An ECSignature object with r,s,v parameters
|
|
*/
|
|
parseECSignature(signature: string): ECSignature {
|
|
assert.isHexString('signature', signature);
|
|
const ecSignatureTypes = [SignatureType.EthSign, SignatureType.EIP712];
|
|
assert.isOneOfExpectedSignatureTypes(signature, ecSignatureTypes);
|
|
|
|
// tslint:disable-next-line:custom-no-magic-numbers
|
|
const vrsHex = signature.slice(0, -2);
|
|
const ecSignature = parseSignatureHexAsVRS(vrsHex);
|
|
|
|
return ecSignature;
|
|
},
|
|
};
|
|
|
|
function parseValidatorSignature(signature: string): ValidatorSignature {
|
|
assert.isOneOfExpectedSignatureTypes(signature, [SignatureType.Validator]);
|
|
// tslint:disable:custom-no-magic-numbers
|
|
const validatorSignature = {
|
|
validatorAddress: signature.slice(-22, -2),
|
|
signature: signature.slice(0, -22),
|
|
};
|
|
// tslint:enable:custom-no-magic-numbers
|
|
return validatorSignature;
|
|
}
|
|
|
|
function parseSignatureHexAsVRS(signatureHex: string): ECSignature {
|
|
const signatureBuffer = ethUtil.toBuffer(signatureHex);
|
|
let v = signatureBuffer[0];
|
|
// HACK: Sometimes v is returned as [0, 1] and sometimes as [27, 28]
|
|
// If it is returned as [0, 1], add 27 to both so it becomes [27, 28]
|
|
const lowestValidV = 27;
|
|
const isProperlyFormattedV = v >= lowestValidV;
|
|
if (!isProperlyFormattedV) {
|
|
v += lowestValidV;
|
|
}
|
|
// signatureBuffer contains vrs
|
|
const vEndIndex = 1;
|
|
const rsIndex = 33;
|
|
const r = signatureBuffer.slice(vEndIndex, rsIndex);
|
|
const sEndIndex = 65;
|
|
const s = signatureBuffer.slice(rsIndex, sEndIndex);
|
|
const ecSignature: ECSignature = {
|
|
v,
|
|
r: ethUtil.bufferToHex(r),
|
|
s: ethUtil.bufferToHex(s),
|
|
};
|
|
return ecSignature;
|
|
}
|
|
|
|
function parseSignatureHexAsRSV(signatureHex: string): ECSignature {
|
|
const { v, r, s } = ethUtil.fromRpcSig(signatureHex);
|
|
const ecSignature: ECSignature = {
|
|
v,
|
|
r: ethUtil.bufferToHex(r),
|
|
s: ethUtil.bufferToHex(s),
|
|
};
|
|
return ecSignature;
|
|
}
|