Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions modules/express/EXTERNAL_SIGNER.md
Original file line number Diff line number Diff line change
@@ -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 `"<walletId>": "<encryptedPrivateKey>"`. 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_<walletId>_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=<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=<encryptedPrivateKeysJson>`.
18 changes: 4 additions & 14 deletions modules/express/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `"<walletId>": "<privateKey>"` 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

Expand All @@ -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
Expand Down
40 changes: 32 additions & 8 deletions modules/express/src/clientRoutes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string> {
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;
Expand Down
8 changes: 6 additions & 2 deletions modules/express/test/unit/clientRoutes/externalSign.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -45,5 +48,6 @@ describe('External signer', () => {
);
readFileStub.restore();
signTransactionStub.restore();
envStub.restore();
});
});