diff --git a/modules/express/EXTERNAL_SIGNER.md b/modules/express/EXTERNAL_SIGNER.md new file mode 100644 index 0000000000..e4186efe2a --- /dev/null +++ b/modules/express/EXTERNAL_SIGNER.md @@ -0,0 +1,46 @@ +# BitGo Express External Signing Mode + +**Note:** External signing mode is currently available only in testnet and is under active development. Breaking changes may be made to it without incrementing the major version until the feature is stabilized and made available in production. + +BitGo Express can be run in an external signing mode, where the signing of transactions is performed in a separate instance of BitGo Express that has access to the private keys. +This may be preferable for users who would like to apply their signature to their transactions using a more secure environment than BitGo SDK or BitGo Express, such as a signer with restricted access or network connectivity. + +![BitGo Express Signer Diagram](express_signer.png) + +### External signer mode setup +To set up BitGo Express with an external signer, a url to the external signer instance of BitGo Express must be provided using the `externalSignerUrl` configuration option. +The corresponding external signer instance of BitGo Express must have `signerMode` set, and `signerFileSystemPath` set to the path of a json containing the private key. +Note that if BitGo Express encounters an `ECONNREFUSED` error when requesting the external signer for a signature, it will retry the request for up to 15 seconds. + +### Encrypted private key format +The JSON file containing the unencrypted private key(s) must be in the format `"": ""`. Note that the `encryptedPrivateKey` must contain escape characters for the double quotations(`"`) in order for it to be parsed correctly. +Here is an example json file containing two wallet IDs and their corresponding encrypted private keys with escape characters: + + ``` +{ +"61f039aad587c2000745c687373e0fa9":"{\"iv\":\"+1u1Y9cvsYuRMeyH2slnXQ==\",\"v\":1,\"iter\":10000,\"ks\":256,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"54kOXTqJ9mc=\",\"ct\":\"JF5wQ82wa1dYyFxFlbHCvK4a+A6MTHdhOqc5uXsz2icWhkY2Lin/3Ab8ZwvwDaR1JYKmC/g1gXIGwVZEOl1M/bRHY420h7sDtmTS6Ebse5NWbF0ItfUJlk6HVATGa+C6mkbaVxJ4kQW/ehnT3riqzU069ATPz8E=\"}", +"61fb21819c54dd000755f8de3a18e46f":"{\"iv\":\"ULAkh1Ia2B2oJbVWRt+xMw==\",\"v\":1,\"iter\":10000,\"ks\":256,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"SVkVei5M1qU=\",\"ct\":\"NxfG1HQWGcrwCHkQh8DKeMaZrRic+SSBQHtuOSsSJzW5MDOpwqDta8PDdh52lp9eqtaY+CGN6rPhaGbeZDrEyV2PoBGeb48GicMTVAehkyoF9mr8edtsWDCxcmmde+1zv3czy2n/bgXYNGvX39D30GDRpfovSYc=\"}" +} +``` + +### Wallet passphrase environment variable +In order for the external signer instance of BitGo Express to decrypt the private key, the wallet passphrase must be set as an environment variable in the format `WALLET__PASSPHRASE`. Note that the wallet passphrase must be set for each wallet. +The environment variable can be set using `export`. For example, the wallet passphrases for the private keys above can be set with the following: + +``` +export WALLET_61f039aad587c2000745c687373e0fa9_PASSPHRASE=wDX058%c4plL1@pP +export WALLET_61fb21819c54dd000755f8de3a18e46f_PASSPHRASE=wDX058%c4plL1@pP +``` + +### External signer mode configuration values +BitGo Express is able to take configuration options from either command line arguments, or via environment variables. + +| Flag Short Name | Flag Long Name | Environment Variable | Default Value | Description | +| --------------- | ---------------------- | ---------------------------------------- | ------------- | ----------------------------------------------------------------------------------------------------------------------- | +| N/A | --externalSignerUrl | `BITGO_EXTERNAL_SIGNER_URL` | N/A | URL specifying the external API to call for remote signing. | +| N/A | --signerMode | `BITGO_SIGNER_MODE ` | N/A | If set, run Express as a remote signer. | +| N/A | --signerFileSystemPath | `BITGO_SIGNER_FILE_SYSTEM_PATH ` | N/A | Local path specifying where an Express signer machine keeps the encrypted user private keys. Required when signerMode is set. | + +#### Example +To start up an instance of BitGo Express that will use an external signer, start BitGo Express with `--externalSignerUrl=`. +To start up an external signing instance of BitGo Express, which will have access to the encrypted private keys and wallet passphrases, start BitGo Express with `--signerMode --signerFileSystemPath=`. diff --git a/modules/express/README.md b/modules/express/README.md index 027c2f923b..a9cca69724 100644 --- a/modules/express/README.md +++ b/modules/express/README.md @@ -166,20 +166,10 @@ BitGo Express currently supports the following proxy protocols: ### External Signing Mode **Note:** External signing mode is currently available only in testnet and is under active development. Breaking changes may be made to it without incrementing the major version until the feature is stabilized and made available in production. -BitGo Express can be run in an external signing mode, where the signing of transactions is performed in a separate instance of BitGo Express that has access to the private keys. This may be preferable for users who would like to apply their signature to their transactions using a more secure environment than BitGo SDK or BitGo Express, such as a signer with restricted access or network connectivity. +BitGo Express can be run in an external signing mode, where the signing of transactions is performed in a separate instance of BitGo Express that has access to the private keys. +This may be preferable for users who would like to apply their signature to their transactions using a more secure environment than BitGo SDK or BitGo Express, such as a signer with restricted access or network connectivity. -To set up BitGo Express with an external signer, a url to the external signer instance of BitGo Express must be provided using the `externalSignerUrl` configuration option. The corresponding external signer instance of BitGo Express must have `signerMode` set, and `signerFileSystemPath` set to the path of a json containing the private key. Note that if BitGo Express encounters an `ECONNREFUSED` error when requesting the external signer for a signature, it will retry the request for up to 15 seconds. - -The JSON file containing the unencrypted private key(s) must be in the format `"": ""` wallet ID and private key. Here is an example json file containing two wallet IDs and their corresponding private keys. - - ``` -{ -"61f039aad587c2000745c687373e0fa9":"xprv9s21ZrQH143K3EuPWCBuqnWxydaQV6et9htQige4EswvcHKEzNmkVmwTwKoadyHzJYppuADB7Us7AbaNLToNvoFoSxuWqndQRYtnNy5DUY2", -"61fb21819c54dd000755f8de3a18e46f":"xprv9s21ZrQH143K2tc1uz6E1rY1WYEaAQyFzY4C3qs4GMyX1uXy6YqdUXrWyKpm3AJmugPR56mC2ggJK5BsCenznJw2nGQ2P9AAEwZVLSVgjGr" -} -``` - -![BitGo Express Signer Diagram](express_signer.png) +For more information please see our [External Signing Mode Documentation](EXTERNAL_SIGNER.md). ## Configuration Values @@ -205,7 +195,7 @@ BitGo Express is able to take configuration options from either command line arg | N/A | --authversion | `BITGO_AUTH_VERSION` | 2 | BitGo Authentication scheme version which should be used form making requests to the BitGo server. Please see the [BitGo API documentation](https://app.bitgo.com/docs) for more info on authentication scheme versions. | | N/A | --externalSignerUrl | `BITGO_EXTERNAL_SIGNER_URL` | N/A | URL specifying the external API to call for remote signing. | | N/A | --signerMode | `BITGO_SIGNER_MODE ` | N/A | If set, run Express as a remote signer. | -| N/A | --signerFileSystemPath | `BITGO_SIGNER_FILE_SYSTEM_PATH ` | N/A | Local path specifying where an Express signer machine keeps the unencrypted user private keys. Required when signerMode is set. | +| N/A | --signerFileSystemPath | `BITGO_SIGNER_FILE_SYSTEM_PATH ` | N/A | Local path specifying where an Express signer machine keeps the encrypted user private keys. Required when signerMode is set. | \[0]: BitGo will also check the additional environment variables for some options for backwards compatibility, but these environment variables should be considered deprecated: * Disable SSL diff --git a/modules/express/src/clientRoutes.ts b/modules/express/src/clientRoutes.ts index 12eeed8f9a..13963be92c 100755 --- a/modules/express/src/clientRoutes.ts +++ b/modules/express/src/clientRoutes.ts @@ -366,19 +366,43 @@ function handleCanonicalAddress(req: express.Request) { return (coin as Coin.Bch | Coin.Bsv | Coin.Ltc).canonicalAddress(address, version || fallbackVersion); } -export async function handleV2Sign(req: express.Request) { - const walletId = req.body.txPrebuild.walletId; - const path = req.config.signerFileSystemPath; - assert(typeof path === 'string'); +function getWalletPwFromEnv(walletId: string): string { + const name = `WALLET_${walletId}_PASSPHRASE`; + const walletPw = process.env[name]; + if (walletPw === undefined) { + throw new Error(`Could not find wallet passphrase ${name} in environment`); + } + return walletPw; +} + +async function getEncryptedPrivKey(path: string, walletId: string): Promise { const privKeyFile = await fs.readFile(path, { encoding: 'utf8' }); - const privKey = JSON.parse(privKeyFile); - if (privKey[walletId] === undefined) { - throw new Error(`Could not find a field for walletId: ${walletId} in ${req.config.signerFileSystemPath}`); + const encryptedPrivKey = JSON.parse(privKeyFile); + if (encryptedPrivKey[walletId] === undefined) { + throw new Error(`Could not find a field for walletId: ${walletId} in ${path}`); + } + return encryptedPrivKey[walletId]; +} + +function decryptPrivKey(bg: BitGo, encryptedPrivKey: string, walletPw: string): string { + try { + return bg.decrypt({ password: walletPw, input: encryptedPrivKey }); + } catch (e) { + throw new Error(`Error when trying to decrypt private key: ${e}`); } +} + +export async function handleV2Sign(req: express.Request) { + const walletId = req.body.txPrebuild.walletId; + const walletPw = getWalletPwFromEnv(walletId); + const privKeyPath = req.config.signerFileSystemPath; + assert(typeof privKeyPath === 'string'); + const encryptedPrivKey = await getEncryptedPrivKey(privKeyPath, walletId); const bitgo = req.bitgo; + const privKey = decryptPrivKey(bitgo, encryptedPrivKey, walletPw); const coin = bitgo.coin(req.params.coin); try { - return await coin.signTransaction({ ...req.body, ...{ prv: privKey[walletId] } }); + return await coin.signTransaction({ ...req.body, prv: privKey }); } catch (error) { console.log('error while signing wallet transaction ', error); throw error; diff --git a/modules/express/test/unit/clientRoutes/externalSign.ts b/modules/express/test/unit/clientRoutes/externalSign.ts index 42c8814b33..ae84a63818 100644 --- a/modules/express/test/unit/clientRoutes/externalSign.ts +++ b/modules/express/test/unit/clientRoutes/externalSign.ts @@ -14,10 +14,13 @@ import { Btc } from 'bitgo/dist/src/v2/coins/btc'; import { BitGo } from 'bitgo'; describe('External signer', () => { - it('should read prv from signerFileSystemPath and pass it to coin.signTransaction', async () => { + it('should read an encrypted prv from signerFileSystemPath and pass it to coin.signTransaction', async () => { const validPrv = - '{"61f039aad587c2000745c687373e0fa9":"xprv9s21ZrQH143K3EuPWCBuqnWxydaQV6et9htQige4EswvcHKEzNmkVmwTwKoadyHzJYppuADB7Us7AbaNLToNvoFoSxuWqndQRYtnNy5DUY2"}'; + '{"61f039aad587c2000745c687373e0fa9":"{\\"iv\\":\\"+1u1Y9cvsYuRMeyH2slnXQ==\\",\\"v\\":1,\\"iter\\":10000,\\"ks\\":256,\\"ts\\":64,\\"mode\\":\\"ccm\\",\\"adata\\":\\"\\",\\"cipher\\":\\"aes\\",\\"salt\\":\\"54kOXTqJ9mc=\\",\\"ct\\":\\"JF5wQ82wa1dYyFxFlbHCvK4a+A6MTHdhOqc5uXsz2icWhkY2Lin/3Ab8ZwvwDaR1JYKmC/g1gXIGwVZEOl1M/bRHY420h7sDtmTS6Ebse5NWbF0ItfUJlk6HVATGa+C6mkbaVxJ4kQW/ehnT3riqzU069ATPz8E=\\"}"}'; const readFileStub = sinon.stub(fs.promises, 'readFile').resolves(validPrv); + const envStub = sinon + .stub(process, 'env') + .value({ WALLET_61f039aad587c2000745c687373e0fa9_PASSPHRASE: 'wDX058%c4plL1@pP' }); const signTransactionStub = sinon.stub(Btc.prototype, 'signTransaction').resolves('signedTx'); const req = { @@ -45,5 +48,6 @@ describe('External signer', () => { ); readFileStub.restore(); signTransactionStub.restore(); + envStub.restore(); }); });