From 32176e78edefa4cf3f5a853c33640604e812a42d Mon Sep 17 00:00:00 2001 From: Darius Parvin Date: Thu, 3 Mar 2022 12:08:30 -0800 Subject: [PATCH 1/3] feat: external signer to read encrypted privkeys - When running in external signer mode, express will expect the private key json file to be in the encrypted format (same as on the keycard but with escape characters). - Express expects the wallet passphrase to be set in the environment with the name WALLET__PASSPHRASE Ticket: BG-43925 --- modules/express/src/clientRoutes.ts | 43 +++++++++++++++---- .../test/unit/clientRoutes/externalSign.ts | 8 +++- 2 files changed, 41 insertions(+), 10 deletions(-) diff --git a/modules/express/src/clientRoutes.ts b/modules/express/src/clientRoutes.ts index 12eeed8f9a..3e5c4d9ba0 100755 --- a/modules/express/src/clientRoutes.ts +++ b/modules/express/src/clientRoutes.ts @@ -366,19 +366,46 @@ 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(encryptedPrivKey: string, walletPw: string): string { + const bg = new BitGo(); + try { + const decrypted = bg.decrypt({ password: walletPw, input: encryptedPrivKey }); + return decrypted; + } catch (e) { + throw new Error(`Error when trying to decrypt private key: ${e}`); } +} + +export async function handleV2Sign(req: express.Request) { + express; + 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 privKey = decryptPrivKey(encryptedPrivKey, walletPw); const bitgo = req.bitgo; 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(); }); }); From c2ce42ac02038e76510d117b2bcb8b29e85fe65d Mon Sep 17 00:00:00 2001 From: Darius Parvin Date: Thu, 3 Mar 2022 12:29:46 -0800 Subject: [PATCH 2/3] docs: update docs for external signer encrypted private key format - private keys should be in encrypted format with escaped characters - wallet passphrase should be set as a environment var - add headers and move diagram up Ticket: BG-43925 --- modules/express/README.md | 30 +++++++++++++++++++++++------- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/modules/express/README.md b/modules/express/README.md index 027c2f923b..935283fc09 100644 --- a/modules/express/README.md +++ b/modules/express/README.md @@ -166,20 +166,36 @@ 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. +![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. -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. +#### 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":"xprv9s21ZrQH143K3EuPWCBuqnWxydaQV6et9htQige4EswvcHKEzNmkVmwTwKoadyHzJYppuADB7Us7AbaNLToNvoFoSxuWqndQRYtnNy5DUY2", -"61fb21819c54dd000755f8de3a18e46f":"xprv9s21ZrQH143K2tc1uz6E1rY1WYEaAQyFzY4C3qs4GMyX1uXy6YqdUXrWyKpm3AJmugPR56mC2ggJK5BsCenznJw2nGQ2P9AAEwZVLSVgjGr" +"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=\"}" } ``` -![BitGo Express Signer Diagram](express_signer.png) +#### 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 +``` + ## Configuration Values @@ -205,7 +221,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 From 2300f14e589e7b573d31f8e6251f0a00db45cee1 Mon Sep 17 00:00:00 2001 From: Darius Parvin Date: Thu, 3 Mar 2022 13:08:54 -0800 Subject: [PATCH 3/3] docs: move external signer docs to new file - create new doc EXTERNAL_SIGNER.md - add example configuration settings Ticket: BG-43925 --- modules/express/EXTERNAL_SIGNER.md | 46 +++++++++++++++++++++++++++++ modules/express/README.md | 28 +----------------- modules/express/src/clientRoutes.ts | 11 +++---- 3 files changed, 51 insertions(+), 34 deletions(-) create mode 100644 modules/express/EXTERNAL_SIGNER.md 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 935283fc09..a9cca69724 100644 --- a/modules/express/README.md +++ b/modules/express/README.md @@ -169,33 +169,7 @@ BitGo Express currently supports the following proxy protocols: 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 -``` - +For more information please see our [External Signing Mode Documentation](EXTERNAL_SIGNER.md). ## Configuration Values diff --git a/modules/express/src/clientRoutes.ts b/modules/express/src/clientRoutes.ts index 3e5c4d9ba0..13963be92c 100755 --- a/modules/express/src/clientRoutes.ts +++ b/modules/express/src/clientRoutes.ts @@ -384,28 +384,25 @@ async function getEncryptedPrivKey(path: string, walletId: string): Promise