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
17 changes: 15 additions & 2 deletions modules/express/src/clientRoutes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ import { RequestTracer } from 'bitgo/dist/src/v2/internal/util';

import { Config } from './config';
import { ApiResponseError } from './errors';
import { promises as fs } from 'fs';
import * as assert from 'assert';

const { version } = require('bitgo/package.json');
const pjson = require('../package.json');
Expand Down Expand Up @@ -372,8 +374,19 @@ function handleV1Sign(req: express.Request) {
throw new Error('not yet implemented');
}

function handleV2Sign(req: express.Request) {
throw new Error('not yet implemented');
export async function handleV2Sign(req: express.Request) {
const path = req.config.signerFileSystemPath;
assert(typeof path === 'string');
const privKeyFile = await fs.readFile(path, { encoding: 'utf8' });
const privKey = JSON.parse(privKeyFile);
const bitgo = req.bitgo;
const coin = bitgo.coin(req.params.coin);
try {
return await coin.signTransaction({ ...req.body, ...privKey });
} catch (error) {
console.log('error while signing wallet transaction ', error);
throw error;
}
}

/**
Expand Down
7 changes: 7 additions & 0 deletions modules/express/src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,10 @@ export class IpcError extends Errors.BitGoJsError {
Object.setPrototypeOf(this, IpcError.prototype);
}
}

export class ExternalSignerConfigError extends Errors.BitGoJsError {
public constructor(message?: string) {
super(message || 'External signer configuration is invalid');
Object.setPrototypeOf(this, ExternalSignerConfigError.prototype);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: this is no longer necessary and can be removed from all other errors defined here. Please remove in a follow up PR though, no need to do this clean up item here

}
}
57 changes: 55 additions & 2 deletions modules/express/src/expressApp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { Config, config } from './config';
const debug = debugLib('bitgo:express');

import { SSL_OP_NO_TLSv1 } from 'constants';
import { IpcError, NodeEnvironmentError, TlsConfigurationError } from './errors';
import { IpcError, NodeEnvironmentError, TlsConfigurationError, ExternalSignerConfigError } from './errors';

import { Environments } from 'bitgo';
import * as clientRoutes from './clientRoutes';
Expand Down Expand Up @@ -155,12 +155,41 @@ export function createBaseUri(config: Config): string {
return `http${tls ? 's' : ''}://${bind}${!isStandardPort ? ':' + port : ''}`;
}

/**
* Check the that the json file containing the external signer private key exists
* @param path
*/
function checkSignerPrvPath(path: string) {
try {
const privKeyFile = fs.readFileSync(path, { encoding: 'utf8' });
const privKey = JSON.parse(privKeyFile);
if (privKey.prv === undefined) {
throw new Error(`required field "prv" is missing`);
}
} catch (e) {
throw new Error(`Failed to parse ${path} - ${e.message}`);
}
}

/**
* Check environment and other preconditions to ensure bitgo-express can start safely
* @param config
*/
function checkPreconditions(config: Config) {
const { env, disableEnvCheck, bind, ipc, disableSSL, keyPath, crtPath, customRootUri, customBitcoinNetwork } = config;
const {
env,
disableEnvCheck,
bind,
ipc,
disableSSL,
keyPath,
crtPath,
customRootUri,
customBitcoinNetwork,
externalSignerUrl,
signerMode,
signerFileSystemPath,
} = config;

// warn or throw if the NODE_ENV is not production when BITGO_ENV is production - this can leak system info from express
if (env === 'prod' && process.env.NODE_ENV !== 'production') {
Expand Down Expand Up @@ -190,6 +219,30 @@ function checkPreconditions(config: Config) {
console.warn(`customRootUri or customBitcoinNetwork is set, but env is '${env}'. Setting env to 'custom'.`);
config.env = 'custom';
}

if (env !== 'test' && (externalSignerUrl !== undefined || signerMode !== undefined)) {
throw new ExternalSignerConfigError('external signer feature is only enabled for test mode.');
}

if (externalSignerUrl !== undefined && (signerMode !== undefined || signerFileSystemPath !== undefined)) {
throw new ExternalSignerConfigError(
'signerMode or signerFileSystemPath is set, but externalSignerUrl is also set.'
);
}

if ((signerMode !== undefined || signerFileSystemPath !== undefined) && !(signerMode && signerFileSystemPath)) {
throw new ExternalSignerConfigError(
'signerMode and signerFileSystemPath must both be set in order to run in external signing mode.'
);
}

if (signerFileSystemPath !== undefined) {
try {
checkSignerPrvPath(signerFileSystemPath);
} catch (e) {
throw e;
}
Comment on lines +240 to +244

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need to catch if you're just going to immediately rethrow - feel free to handle in a follow up PR to clean up some other nits as well.

Suggested change
try {
checkSignerPrvPath(signerFileSystemPath);
} catch (e) {
throw e;
}
checkSignerPrvPath(signerFileSystemPath);

}
}

export function setupRoutes(app: express.Application, config: Config): void {
Expand Down
94 changes: 93 additions & 1 deletion modules/express/test/unit/bitgoExpress.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ import * as clientRoutes from '../../src/clientRoutes';
describe('Bitgo Express', function () {

describe('server initialization', function () {
const validPrvJSON =
'{"prv":"xprv9s21ZrQH143K3EuPWCBuqnWxydaQV6et9htQige4EswvcHKEzNmkVmwTwKoadyHzJYppuADB7Us7AbaNLToNvoFoSxuWqndQRYtnNy5DUY2"}';

it('should require NODE_ENV to be production when running against prod env', function () {
const envStub = sinon.stub(process, 'env').value({ NODE_ENV: 'production' });
Expand Down Expand Up @@ -372,7 +374,7 @@ describe('Bitgo Express', function () {
it('should only call setupAPIRoutes when running in regular mode', () => {
const args: any = {
env: 'test',
signerMode: '',
signerMode: undefined,
};

const apiStub = sinon.stub(clientRoutes, 'setupAPIRoutes');
Expand All @@ -389,16 +391,106 @@ describe('Bitgo Express', function () {
const args: any = {
env: 'test',
signerMode: 'signerMode',
signerFileSystemPath: 'signerFileSystemPath',
};

const apiStub = sinon.stub(clientRoutes, 'setupAPIRoutes');
const signerStub = sinon.stub(clientRoutes, 'setupSigningRoutes');
const readFileStub = sinon.stub(fs, 'readFileSync').returns(validPrvJSON);

expressApp(args);
signerStub.should.have.been.calledOnce();
apiStub.called.should.be.false();
apiStub.restore();
signerStub.restore();
readFileStub.restore();
});

it('should require a signerFileSystemPath and signerMode are both set when running in signer mode', function () {
const args: any = {
env: 'test',
signerMode: 'signerMode',
signerFileSystemPath: undefined,
};

(() => expressApp(args)).should.throw({
name: 'ExternalSignerConfigError',
message: 'signerMode and signerFileSystemPath must both be set in order to run in external signing mode.'
});

args.signerMode = undefined;
args.signerFileSystemPath = 'signerFileSystemPath';
(() => expressApp(args)).should.throw({
name: 'ExternalSignerConfigError',
message: 'signerMode and signerFileSystemPath must both be set in order to run in external signing mode.'
});

const readFileStub = sinon.stub(fs, 'readFileSync').returns(validPrvJSON);
args.signerMode = 'signerMode';
(() => expressApp(args)).should.not.throw();

readFileStub.restore();
});

it('should require that an externalSignerUrl and signerMode are not both set', function () {
const args: any = {
env: 'test',
signerMode: 'signerMode',
externalSignerUrl: 'externalSignerUrl',
};
(() => expressApp(args)).should.throw({
name: 'ExternalSignerConfigError',
message: 'signerMode or signerFileSystemPath is set, but externalSignerUrl is also set.'
});

args.signerMode = undefined;
(() => expressApp(args)).should.not.throw();
});

it('should require that an signerFileSystemPath contains a json with a prv field', function () {
const args: any = {
env: 'test',
signerMode: 'signerMode',
signerFileSystemPath: 'invalidSignerFileSystemPath',
};
(() => expressApp(args)).should.throw();

const invalidPrv =
'{"invalidField":"invalidPrivKey"}';
const readInvalidStub = sinon.stub(fs, 'readFileSync').returns(invalidPrv);
(() => expressApp(args)).should.throw(`Failed to parse ${args.signerFileSystemPath} - required field "prv" is missing`);
readInvalidStub.restore();

const readValidStub = sinon.stub(fs, 'readFileSync').returns(validPrvJSON);
(() => expressApp(args)).should.not.throw();
readValidStub.restore();
});

it('should require express to be in test mode when using the external signer feature', function () {
const readValidStub = sinon.stub(fs, 'readFileSync').returns(validPrvJSON);

const args: any = {
env: 'notTestMode',
signerMode: 'signerMode',
signerFileSystemPath: 'signerFileSystemPath',
};
(() => expressApp(args)).should.throw({
name: 'ExternalSignerConfigError',
message: 'external signer feature is only enabled for test mode.'
});

args.signerMode = undefined;
args.signerFileSystemPath = undefined;
args.externalSignerUrl = 'externalSignerUrl';
(() => expressApp(args)).should.throw({
name: 'ExternalSignerConfigError',
message: 'external signer feature is only enabled for test mode.'
});

args.env = 'test';
(() => expressApp(args)).should.not.throw();

readValidStub.restore();
});
});
});
44 changes: 44 additions & 0 deletions modules/express/test/unit/clientRoutes/externalSign.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/**
* @prettier
*/
import * as sinon from 'sinon';

import 'should-http';
import 'should-sinon';
import '../../lib/asserts';

import * as express from 'express';
import { handleV2Sign } from '../../../src/clientRoutes';
import * as fs from 'fs';
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 () => {
const validPrv =
'{"prv":"xprv9s21ZrQH143K3EuPWCBuqnWxydaQV6et9htQige4EswvcHKEzNmkVmwTwKoadyHzJYppuADB7Us7AbaNLToNvoFoSxuWqndQRYtnNy5DUY2"}';
const readFileStub = sinon.stub(fs.promises, 'readFile').resolves(validPrv);
const signTransactionStub = sinon.stub(Btc.prototype, 'signTransaction').resolves('signedTx');

const req = {
bitgo: new BitGo({ env: 'test' }),
params: {
coin: 'tbtc',
},
config: {
signerFileSystemPath: 'signerFileSystemPath',
},
} as unknown as express.Request;

await handleV2Sign(req);

readFileStub.should.be.calledOnceWith('signerFileSystemPath');
signTransactionStub.should.be.calledOnceWith(
sinon.match({
prv: 'xprv9s21ZrQH143K3EuPWCBuqnWxydaQV6et9htQige4EswvcHKEzNmkVmwTwKoadyHzJYppuADB7Us7AbaNLToNvoFoSxuWqndQRYtnNy5DUY2',
})
);
readFileStub.restore();
signTransactionStub.restore();
});
});