diff --git a/federator/config/config.sample.js b/federator/config/config.sample.js index 0a6e3f169..df803105d 100644 --- a/federator/config/config.sample.js +++ b/federator/config/config.sample.js @@ -9,4 +9,6 @@ module.exports = { etherscanApiKey: '', runHeartbeatEvery: 1, // In hours endpointsPort: 5000, // Server port + hsmPort: 6000, // [HSM] signing service port + hsmHost: '127.0.0.1' // [HSM] signing service host } diff --git a/federator/src/lib/HSM.js b/federator/src/lib/HSM.js new file mode 100644 index 000000000..30190a03d --- /dev/null +++ b/federator/src/lib/HSM.js @@ -0,0 +1,50 @@ +const net = require('net'); + +function hsmPayloadBuilder(command, keyId, txnHash) { + return `{"command":"${command}","keyId":"${keyId}","message":{"hash":"${txnHash}"},"version":2}`; +} + +module.exports = class HSM { + constructor({ + host = '127.0.0.1', + port = 6000, + }, logger) { + this.host = host; + this.port = port; + this.logger = logger; + this.client = null; + } + + async receive() { + return new Promise((resolve, reject) => { + this.client.on('data', (data) => { + resolve(data.toString()); + this.client.end(); + }) + + this.client.on('end', () => { + resolve(`connection to ${this.host}:${this.port} closed`) + }) + + this.client.on('error', (err) => { + reject(`Error: ${err.message}`) + }) + }) + } + + send(msgToSign = '') { + const payload = hsmPayloadBuilder(`sign`, `m/44'/60'/0'/0/0`, msgToSign); + return this.client.write(`${payload}\n`); + } + + async connectSendAndReceive(msgToSign) { + try { + this.client = net.connect(this.port, this.host); + this.send(msgToSign); + return this.receive(); + } catch(err) { + this.logger.error(`HSM (connectSendAndReceive)`, err); + throw err; + } + } +} \ No newline at end of file diff --git a/federator/src/lib/TransactionSender.js b/federator/src/lib/TransactionSender.js index 6fdb7c94d..670c715d6 100644 --- a/federator/src/lib/TransactionSender.js +++ b/federator/src/lib/TransactionSender.js @@ -1,9 +1,9 @@ - const Tx = require('ethereumjs-tx'); const ethUtils = require('ethereumjs-util'); const utils = require('./utils'); const fs = require('fs'); const axios = require('axios'); +const HSM = require('./HSM'); module.exports = class TransactionSender { constructor(client, logger, config) { @@ -13,6 +13,10 @@ module.exports = class TransactionSender { this.manuallyCheck = `${config.storagePath || __dirname}/manuallyCheck.txt`; this.etherscanApiKey = config.etherscanApiKey; this.debuggingMode = false; + this.hsm = new HSM({ + port: config.hsmPort, + host: config.hsmHost + }, this.logger); } async getNonce(address) { @@ -126,12 +130,32 @@ module.exports = class TransactionSender { return rawTx; } - signRawTransaction(rawTx, privateKey) { + async signRawTransaction(rawTx, privateKey, useHSM) { let tx = new Tx(rawTx); - tx.sign(utils.hexStringToBuffer(privateKey)); + if(!useHSM) { + tx.sign(utils.hexStringToBuffer(privateKey)); + } else { + const txHash = tx.hash(false).toString('hex'); + const { + errorcode, + signature: { + r, + s + } = { r: '0x0', s: '0x0' } + } = JSON.parse(await this.hsm.connectSendAndReceive(txHash)); + + if(errorcode != 0) { + throw new Error(`error while signing txn with HSM`) + } + + tx.r = Buffer.from(r, 'hex'); + tx.s = Buffer.from(s, 'hex'); + tx.v = Buffer.from((this.getChainId() * 2 + 8).toString()); + } return tx; } + async getAddress(privateKey) { let address = null; if (privateKey && privateKey.length) { @@ -170,15 +194,15 @@ module.exports = class TransactionSender { return response.data; } - async sendTransaction(to, data, value, privateKey) { + async sendTransaction(to, data, value, privateKey, useHSM = false) { const chainId = await this.getChainId(); let txHash; let receipt; + let from = await this.getAddress(privateKey); + let rawTx = await this.createRawTransaction(from, to, data, value); try { - let from = await this.getAddress(privateKey); - let rawTx = await this.createRawTransaction(from, to, data, value); - if (privateKey && privateKey.length) { - let signedTx = this.signRawTransaction(rawTx, privateKey); + if (privateKey && privateKey.length || useHSM) { + let signedTx = await this.signRawTransaction(rawTx, privateKey, useHSM); const serializedTx = ethUtils.bufferToHex(signedTx.serialize()); receipt = await this.client.eth.sendSignedTransaction(serializedTx).once('transactionHash', async (hash) => { txHash = hash; diff --git a/federator/src/lib/utils.js b/federator/src/lib/utils.js index 0041926b4..872f3f4ec 100644 --- a/federator/src/lib/utils.js +++ b/federator/src/lib/utils.js @@ -177,6 +177,7 @@ async function evm_mine(iterations, web3Instance = null) { }; }; + module.exports = { asyncMine, evm_mine, @@ -193,5 +194,5 @@ module.exports = { zeroHash: '0x0000000000000000000000000000000000000000000000000000000000000000', retry, retry3Times, - getHeartbeatPollingInterval + getHeartbeatPollingInterval, } diff --git a/federator/test/TransactionSender.test.js b/federator/test/TransactionSender.test.js index d1214a3fa..000a9c95e 100644 --- a/federator/test/TransactionSender.test.js +++ b/federator/test/TransactionSender.test.js @@ -74,4 +74,40 @@ describe('TransactionSender module tests', () => { expect(result).toEqual(expectedAddr.toLocaleLowerCase()); }); -}); \ No newline at end of file + it('should sign the same with HSM and web3', async () => { + const rawTx = { + chainId: 5777, + gasPrice: '0x6fc23ac00', + value: '0x0', + to: '0x557b77f7B280006f7732dCc123C3A966F5Fe1372', + data: '0x7ff4657e000000000000000000000000de451f57d061b915525736937d0f5d24c551edd1000000000000000000000000000000000000000000000000000000000000004000000000000000000000000013263f73dcbe9b123a9ea32c13040b2becfe1e5c00000000000000000000000013263f73dcbe9b123a9ea32c13040b2becfe1e5c000000000000000000000000000000000000000000000000125195019f840000157f354383710432cdc131e73815a179a4e858ea304e4916b0f4d1db6553a7a70612db9f2ee9b8d7078e8f00338f600080ebc2a1e27f39376f54f0f6d7fb73750000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000000044d41494e00000000000000000000000000000000000000000000000000000000', + from: '0x57093C0C2aFEACaF7D677356c3eAC3E99933F7C0', + nonce: '0x49', + r: 0, + s: 0, + gas: '0x284d8' + } + + const pk = `3f28f888373e9ad1651a1227a5efdc0d7ea55bce6de3b5448de56c8588c6bd4d`; + const pk2 = `dfac7a2bfe2cd7f7fc8caffd65995300eb0e1a652502147da8d7a9e5bce16ac2`; + const from = `0x3444f14CbC7081ADEd7203E32E65304D17fe3bdA`; + const sender = new TransactionSender(web3Mock, logger, { + hsmPort: 6000, + hsmHost: '127.0.0.1' + }); + + const signedRawTransaction = await sender.signRawTransaction(rawTx, pk2, false); + const r = signedRawTransaction.r.toString('hex'); + const s = signedRawTransaction.s.toString('hex'); + const v = signedRawTransaction.v.toString('hex'); + + const signedHsmRawTransaction = await sender.signRawTransaction(rawTx, pk2, true); + const rHSM = signedHsmRawTransaction.r.toString('hex'); + const sHSM = signedHsmRawTransaction.s.toString('hex'); + const vHSM = signedHsmRawTransaction.v.toString('hex'); + + expect(rHSM).toEqual(r); + expect(sHSM).toEqual(s); + }); + +});