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
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,6 @@ curl -X POST http://localhost:3081/ping/advancedWalletManager
| ------------------------------ | ---------------------------------- | ------- | -------- |
| `ADVANCED_WALLET_MANAGER_PORT` | Port to listen on | `3080` | ❌ |
| `KEY_PROVIDER_URL` | URL to your key provider API implementation | - | ✅ |
| `SIGNING_MODE` | Signing mode (`local` or `external`). Use `external` to delegate key generation and signing to your key provider — the private key never leaves the HSM. | `local` | ❌ |
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

un-committing this for now while still in dev


> **Note:** The `KEY_PROVIDER_URL` points to your implementation of the key provider API interface. You must implement this interface to connect your KMS/HSM. See [Prerequisites](#prerequisites) for the specification and examples.

Expand Down
126 changes: 126 additions & 0 deletions src/__tests__/api/advancedWalletManager/keyProviderClient.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { AppMode, AdvancedWalletManagerConfig, TlsMode, SigningMode } from '../../../initConfig';
import { app as expressApp } from '../../../advancedWalletManagerApp';
import { KeyProviderClient } from '../../../advancedWalletManager/keyProviderClient/keyProviderClient';

import express from 'express';
import nock from 'nock';
Expand Down Expand Up @@ -111,3 +112,128 @@ describe('postMpcV2Key', () => {
);
});
});

describe('KeyProviderClient.generateKey', () => {
const keyProviderUrl = 'http://key-provider.invalid';
const endPointPath = '/key/generate';
const params = { coin: 'hteth', source: 'user' as const, type: 'independent' as const };
const mockResponse = { pub: 'xpub661MyMwAq', coin: 'hteth', source: 'user', type: 'independent' };
let client: KeyProviderClient;

before(() => {
nock.disableNetConnect();
client = new KeyProviderClient({
appMode: AppMode.ADVANCED_WALLET_MANAGER,
signingMode: SigningMode.LOCAL,
port: 0,
bind: 'localhost',
timeout: 60000,
httpLoggerFile: '',
keyProviderUrl,
tlsMode: TlsMode.DISABLED,
clientCertAllowSelfSigned: true,
});
});

afterEach(() => nock.cleanAll());

it('should call POST /key/generate with correct params and return response', async () => {
const nockMocked = nock(keyProviderUrl).post(endPointPath, params).reply(200, mockResponse);

const result = await client.generateKey(params);

result.should.have.property('pub', mockResponse.pub);
result.should.have.property('coin', mockResponse.coin);
result.should.have.property('source', mockResponse.source);
result.should.have.property('type', mockResponse.type);
nockMocked.done();
});

[
{
url: endPointPath,
statusCode: 400,
mockedError: 'bad request',
expectedError: 'bad request',
},
{ url: endPointPath, statusCode: 404, mockedError: 'not found', expectedError: 'not found' },
{ url: endPointPath, statusCode: 409, mockedError: 'conflict', expectedError: 'conflict' },
{
url: endPointPath,
statusCode: 500,
mockedError: 'internal error',
expectedError: 'internal error',
},
].forEach(({ url, statusCode, mockedError, expectedError }) => {
it(`should bubble up ${statusCode} errors`, async () => {
const nockMocked = nock(keyProviderUrl)
.post(url)
.reply(statusCode, { message: mockedError })
.persist();
await client.generateKey(params).should.be.rejectedWith(expectedError);
nockMocked.done();
});
});
});

describe('KeyProviderClient.sign', () => {
const keyProviderUrl = 'http://key-provider.invalid';
const endPointPath = '/sign';
const params = {
pub: 'xpub661MyMwAq',
source: 'user' as const,
signablePayload: 'deadbeef',
algorithm: 'ecdsa',
};
const mockResponse = { signature: 'signedpsbt' };
let client: KeyProviderClient;

before(() => {
nock.disableNetConnect();
client = new KeyProviderClient({
appMode: AppMode.ADVANCED_WALLET_MANAGER,
signingMode: SigningMode.LOCAL,
port: 0,
bind: 'localhost',
timeout: 60000,
httpLoggerFile: '',
keyProviderUrl,
tlsMode: TlsMode.DISABLED,
clientCertAllowSelfSigned: true,
});
});

afterEach(() => nock.cleanAll());

it('should call POST /sign with correct params and return signature', async () => {
const n = nock(keyProviderUrl).post(endPointPath, params).reply(200, mockResponse);

const result = await client.sign(params);

result.should.have.property('signature', mockResponse.signature);
n.done();
});

it('should throw if response has no signature', async () => {
nock(keyProviderUrl).post(endPointPath).reply(200, {});
await client
.sign(params)
.should.be.rejectedWith(/key provider returned unexpected response when signing/);
});

[
{ statusCode: 400, mockedError: 'bad request', expectedError: 'bad request' },
{ statusCode: 404, mockedError: 'not found', expectedError: 'not found' },
{ statusCode: 409, mockedError: 'conflict', expectedError: 'conflict' },
{ statusCode: 500, mockedError: 'internal error', expectedError: 'internal error' },
].forEach(({ statusCode, mockedError, expectedError }) => {
it(`should bubble up ${statusCode} errors`, async () => {
const nockMocked = nock(keyProviderUrl)
.post(endPointPath)
.reply(statusCode, { message: mockedError })
.persist();
await client.sign(params).should.be.rejectedWith(expectedError);
nockMocked.done();
});
});
});
102 changes: 102 additions & 0 deletions src/__tests__/api/advancedWalletManager/postIndependentKey.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import express from 'express';
import * as sinon from 'sinon';
import coinFactory from '../../../shared/coinFactory';
import { BaseCoin } from '@bitgo-beta/sdk-core';
import { CoinFamily } from '@bitgo-beta/statics';

describe('postIndependentKey', () => {
let cfg: AdvancedWalletManagerConfig;
Expand Down Expand Up @@ -85,6 +86,7 @@ describe('postIndependentKey', () => {
it('should fail if there is an error in creating the public and private key pairs', async () => {
const coinStub = sinon.stub(coinFactory, 'getCoin').returns(
Promise.resolve({
getFamily: () => CoinFamily.ETH,
keychains: () => ({
create: () => ({}),
}),
Expand All @@ -102,3 +104,103 @@ describe('postIndependentKey', () => {
coinStub.restore();
});
});

describe('postIndependentKey — external signing mode', () => {
let app: express.Application;
let agent: request.SuperAgentTest;
let coinStub: sinon.SinonStub;

const keyProviderUrl = 'http://key-provider.invalid';
const coin = 'tbtc';
const accessToken = 'test-token';
const mockGenerateKeyResponse = {
pub: 'xpub661MyMwAq',
coin,
source: 'user',
type: 'independent',
};

const utxoCoinStub = {
getFamily: () => CoinFamily.BTC,
getFullName: () => 'Test Bitcoin',
keychains: () => ({ create: sinon.stub().returns({ pub: 'xpub...', prv: 'xprv...' }) }),
} as unknown as BaseCoin;

const nonUtxoCoinStub = {
getFamily: () => CoinFamily.ETH,
getFullName: () => 'Test Ethereum',
keychains: () => ({ create: sinon.stub().returns({ pub: 'xpub...', prv: 'xprv...' }) }),
} as unknown as BaseCoin;

before(() => {
nock.disableNetConnect();
nock.enableNetConnect('127.0.0.1');

app = advancedWalletManagerApp({
appMode: AppMode.ADVANCED_WALLET_MANAGER,
signingMode: SigningMode.EXTERNAL,
port: 0,
bind: 'localhost',
timeout: 60000,
httpLoggerFile: '',
keyProviderUrl,
tlsMode: TlsMode.DISABLED,
clientCertAllowSelfSigned: true,
});
agent = request.agent(app);
});

afterEach(() => {
nock.cleanAll();
coinStub?.restore();
});

it('should call POST /key/generate for UTXO coin and not call POST /key', async () => {
coinStub = sinon.stub(coinFactory, 'getCoin').resolves(utxoCoinStub);
const externalKeyGeneratorNock = nock(keyProviderUrl)
.post('/key/generate', { coin, source: 'user', type: 'independent' })
.reply(200, mockGenerateKeyResponse);
const localKeyGeneratorNock = nock(keyProviderUrl).post('/key').reply(200, {});

const response = await agent
.post(`/api/${coin}/key/independent`)
.set('Authorization', `Bearer ${accessToken}`)
.send({ source: 'user' });

response.status.should.equal(200);
response.body.should.have.property('pub', mockGenerateKeyResponse.pub);
externalKeyGeneratorNock.done();
localKeyGeneratorNock.isDone().should.equal(false);
});

it('should not call coin.keychains().create() in external mode for UTXO coin', async () => {
const createSpy = sinon.spy();
const utxoWithSpy = {
...utxoCoinStub,
keychains: () => ({ create: createSpy }),
} as unknown as BaseCoin;
coinStub = sinon.stub(coinFactory, 'getCoin').resolves(utxoWithSpy);
nock(keyProviderUrl).post('/key/generate').reply(200, mockGenerateKeyResponse);

await agent
.post(`/api/${coin}/key/independent`)
.set('Authorization', `Bearer ${accessToken}`)
.send({ source: 'backup' });

createSpy.called.should.equal(false);
});

it('should fall through to local path for non-UTXO coin in external mode', async () => {
coinStub = sinon.stub(coinFactory, 'getCoin').resolves(nonUtxoCoinStub);
const externalKeyGeneratorNock = nock(keyProviderUrl).post('/key/generate').reply(200, {});
nock(keyProviderUrl).post('/key').reply(200, mockGenerateKeyResponse);

const response = await agent
.post(`/api/${coin}/key/independent`)
.set('Authorization', `Bearer ${accessToken}`)
.send({ source: 'user' });

response.status.should.equal(200);
externalKeyGeneratorNock.isDone().should.equal(false);
});
});
57 changes: 32 additions & 25 deletions src/__tests__/api/advancedWalletManager/recoveryMpcV2.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import * as sinon from 'sinon';
import * as configModule from '../../../initConfig';
import { DklsTypes, DklsUtils } from '@bitgo-beta/sdk-lib-mpc';

describe('recoveryMpcV2', async () => {
describe('recoveryMpcV2', () => {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

bug - describe shouldn't use async

let cfg: AdvancedWalletManagerConfig;
let app: express.Application;
let agent: request.SuperAgentTest;
Expand All @@ -23,36 +23,43 @@ describe('recoveryMpcV2', async () => {
// sinon stubs
let configStub: sinon.SinonStub;

// key provider nocks setup
const [userShare, backupShare] = await DklsUtils.generateDKGKeyShares();
const userKeyShare = userShare.getKeyShare().toString('base64');
const backupKeyShare = backupShare.getKeyShare().toString('base64');
const commonKeychain = DklsTypes.getCommonKeychain(userShare.getKeyShare());

const mockKeyProviderUserResponse = {
prv: JSON.stringify(userKeyShare),
pub: commonKeychain,
source: 'user',
type: 'tss',
};

const mockKeyProviderBackupResponse = {
prv: JSON.stringify(backupKeyShare),
pub: commonKeychain,
source: 'backup',
type: 'tss',
};
const input = {
txHex:
'02f6824268018502540be4008504a817c80083030d409443442e403d64d29c4f64065d0c1a0e8edc03d6c88801550f7dca700000823078c0',
pub: commonKeychain,
};
// key provider nocks setup — initialized in before()
let commonKeychain!: string;
let mockKeyProviderUserResponse: { prv: string; pub: string; source: string; type: string };
let mockKeyProviderBackupResponse: { prv: string; pub: string; source: string; type: string };
let input: { txHex: string; pub: string };

before(async () => {
// nock config
nock.disableNetConnect();
nock.enableNetConnect('127.0.0.1');

// generate DKG key shares
const [userShare, backupShare] = await DklsUtils.generateDKGKeyShares();
const userKeyShare = userShare.getKeyShare().toString('base64');
const backupKeyShare = backupShare.getKeyShare().toString('base64');
commonKeychain = DklsTypes.getCommonKeychain(userShare.getKeyShare());

mockKeyProviderUserResponse = {
prv: JSON.stringify(userKeyShare),
pub: commonKeychain,
source: 'user',
type: 'tss',
};

mockKeyProviderBackupResponse = {
prv: JSON.stringify(backupKeyShare),
pub: commonKeychain,
source: 'backup',
type: 'tss',
};

input = {
txHex:
'02f6824268018502540be4008504a817c80083030d409443442e403d64d29c4f64065d0c1a0e8edc03d6c88801550f7dca700000823078c0',
pub: commonKeychain,
};

// app config
cfg = {
appMode: AppMode.ADVANCED_WALLET_MANAGER,
Expand Down
Loading
Loading