import { ExchangeContract, IValidatorContract, IWalletContract } from '@0x/abi-gen-wrappers'; import * as artifacts from '@0x/contract-artifacts'; import { schemas } from '@0x/json-schemas'; import { ECSignature, Order, SignatureType, SignedOrder, ValidatorSignature } from '@0x/types'; import { Web3Wrapper } from '@0x/web3-wrapper'; import { Provider } from 'ethereum-types'; import * as ethUtil from 'ethereumjs-util'; import * as _ from 'lodash'; import { assert } from './assert'; import { eip712Utils } from './eip712_utils'; import { orderHashUtils } from './order_hash'; 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 { 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); 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 { 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 { 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(0, -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 { 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 order and returns a SignedOrder. First `eth_signTypedData` is requested * then a fallback to `eth_sign` if not available on the supplied provider. * @param order The Order to sign. * @param signerAddress The hex encoded Ethereum address you wish to sign it with. This address * must be available via the supplied Provider. * @return A SignedOrder containing the order and Elliptic curve signature with Signature Type. */ async ecSignOrderAsync(provider: Provider, order: Order, signerAddress: string): Promise { assert.doesConformToSchema('order', order, schemas.orderSchema, [schemas.hexSchema]); try { const signedOrder = await signatureUtils.ecSignTypedDataOrderAsync(provider, order, signerAddress); return signedOrder; } catch (err) { // HACK: We are unable to handle specific errors thrown since provider is not an object // under our control. It could be Metamask Web3, Ethers, or any general RPC provider. // We check for a user denying the signature request in a way that supports Metamask and // Coinbase Wallet. Unfortunately for signers with a different error message, // they will receive two signature requests. if (err.message.includes('User denied message signature')) { throw err; } const orderHash = orderHashUtils.getOrderHashHex(order); const signatureHex = await signatureUtils.ecSignHashAsync(provider, orderHash, signerAddress); const signedOrder = { ...order, signature: signatureHex, }; return signedOrder; } }, /** * Signs an order using `eth_signTypedData` and returns a SignedOrder. * @param order The Order to sign. * @param signerAddress The hex encoded Ethereum address you wish to sign it with. This address * must be available via the supplied Provider. * @return A SignedOrder containing the order and Elliptic curve signature with Signature Type. */ async ecSignTypedDataOrderAsync(provider: Provider, order: Order, signerAddress: string): Promise { assert.isWeb3Provider('provider', provider); assert.isETHAddressHex('signerAddress', signerAddress); assert.doesConformToSchema('order', order, schemas.orderSchema, [schemas.hexSchema]); const web3Wrapper = new Web3Wrapper(provider); await assert.isSenderAddressAsync('signerAddress', signerAddress, web3Wrapper); const normalizedSignerAddress = signerAddress.toLowerCase(); const typedData = eip712Utils.createOrderTypedData(order); try { const signature = await web3Wrapper.signTypedDataAsync(normalizedSignerAddress, typedData); const ecSignatureRSV = parseSignatureHexAsRSV(signature); const signatureBuffer = Buffer.concat([ ethUtil.toBuffer(ecSignatureRSV.v), ethUtil.toBuffer(ecSignatureRSV.r), ethUtil.toBuffer(ecSignatureRSV.s), ethUtil.toBuffer(SignatureType.EIP712), ]); const signatureHex = `0x${signatureBuffer.toString('hex')}`; return { ...order, signature: signatureHex, }; } catch (err) { // Detect if Metamask to transition users to the MetamaskSubprovider if ((provider as any).isMetaMask) { throw new Error(OrderError.InvalidMetamaskSigner); } else { throw err; } } }, /** * Signs a hash using `eth_sign` and returns its elliptic curve signature and signature type. * @param msgHash Hex encoded message to sign. * @param signerAddress The hex encoded Ethereum address you wish to sign it with. This address * must be available via the supplied Provider. * @return A hex encoded string containing the Elliptic curve signature generated by signing the msgHash and the Signature Type. */ async ecSignHashAsync(provider: Provider, msgHash: string, signerAddress: string): Promise { assert.isWeb3Provider('provider', provider); assert.isHexString('msgHash', msgHash); assert.isETHAddressHex('signerAddress', signerAddress); const web3Wrapper = new Web3Wrapper(provider); await assert.isSenderAddressAsync('signerAddress', signerAddress, web3Wrapper); const normalizedSignerAddress = signerAddress.toLowerCase(); const signature = await web3Wrapper.signMessageAsync(normalizedSignerAddress, msgHash); const prefixedMsgHashHex = signatureUtils.addSignedMessagePrefix(msgHash); // 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); 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); return convertedSignatureHex; } } // Detect if Metamask to transition users to the MetamaskSubprovider if ((provider as any).isMetaMask) { throw new Error(OrderError.InvalidMetamaskSigner); } else { throw new Error(OrderError.InvalidSignature); } }, /** * Combines ECSignature with V,R,S and the EthSign signature type for use in 0x protocol * @param ecSignature The ECSignature of the signed data * @return Hex encoded string of signature (v,r,s) with Signature Type */ convertECSignatureToSignatureHex(ecSignature: ECSignature): string { const signatureBuffer = Buffer.concat([ ethUtil.toBuffer(ecSignature.v), ethUtil.toBuffer(ecSignature.r), ethUtil.toBuffer(ecSignature.s), ]); const signatureHex = `0x${signatureBuffer.toString('hex')}`; const signatureWithType = signatureUtils.convertToSignatureWithType(signatureHex, SignatureType.EthSign); 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 * @return Prefixed message */ addSignedMessagePrefix(message: string): string { assert.isString('message', message); const msgBuff = ethUtil.toBuffer(message); const prefixedMsgBuff = ethUtil.hashPersonalMessage(msgBuff); const prefixedMsgHex = ethUtil.bufferToHex(prefixedMsgBuff); return prefixedMsgHex; }, /** * Parse a 0x protocol hex-encoded signature string into its 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; }