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
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ describe('postMpcV2Key', () => {
const coin = 'hteth';
const accessToken = 'test-token';

// sinon stubs
let configStub: sinon.SinonStub;
// sinon sandbox
const sandbox = sinon.createSandbox();

before(() => {
// nock config
Expand All @@ -42,7 +42,7 @@ describe('postMpcV2Key', () => {
clientCertAllowSelfSigned: true,
};

configStub = sinon.stub(configModule, 'initConfig').returns(cfg);
sandbox.stub(configModule, 'initConfig').returns(cfg);

// app setup
app = advancedWalletManagerApp(cfg);
Expand Down Expand Up @@ -93,7 +93,7 @@ describe('postMpcV2Key', () => {
});

after(() => {
configStub.restore();
sandbox.restore();
});

it('should be able to create a new MPC V2 wallet', async () => {
Expand Down
79 changes: 79 additions & 0 deletions src/__tests__/api/advancedWalletManager/recoveryMpc.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,85 @@ describe('recoveryMpc', () => {
});
});

describe('EdDSA dual-KMS recovery', () => {
it('should route backup key retrieval to backup KMS when configured', async () => {
const primaryKmsUrl = 'http://primary-kms.invalid';
const backupKmsUrl = 'http://backup-kms.invalid';

const commonKeychain =
'b6f5fb808f538a32735a89609e98fab75690a2c79b26f50a54c4cbf0fbca287138b733783f1590e12b4916ef0f6053b22044860117274bda44bd5d711855f174';

const mockKmsUserResponse = {
prv: '{"uShare":{"i":1,"t":2,"n":3,"y":"85aa6462d927329418f70f6d0863cf6cf33e7da2934f935e5927f1b13062d779","seed":"2f55c80fd6b5583dcde8037b2ee461d2e7d445a4d3e7a9b2a0d3d00b5f534169","chaincode":"66e80f2bf41a5706608352d51ceb07a5aa1729cab6c6993c124d5731546ed9a1"},"bitgoYShare":{"i":1,"j":3,"y":"483e53b72de3aa893df698d0b20b20777fb3d2716cc8483a9e9797174fd52b16","v":"e70696459e46434a2a12cc988e3ae714a61fe96da8a6764d058b849cab50d6dc","u":"49abf8144d265a77cf6d098eff784d6ce56ec77a182f6b39f47d5d8e28f2a802","chaincode":"797348468202f1d7fede0a7851f80162b02e7da306e65075dd864b6789b9bc5b"},"backupYShare":{"i":1,"j":2,"y":"249a9798d0064a989a16cd8f479edf09ffaee73f4175d2ac555ba90ff41b89da","v":"98e31d2b643e40060ba344c6a41fc096ea7e39a1ae879f65e4af645870e90ee0","u":"ac047b1bceab2e1a42d97ab540b39176e545d9c0af4a192aee8e1dae91a4240b","chaincode":"585bdc05c8f84802cbe7b9a1a07d4aa9c5fede93597a622854e9bad83a2d5b78"}}',
pub: commonKeychain,
source: 'user',
type: 'tss',
};

const mockKmsBackupResponse = {
prv: '{"uShare":{"i":2,"t":2,"n":3,"y":"249a9798d0064a989a16cd8f479edf09ffaee73f4175d2ac555ba90ff41b89da","seed":"abab5be2b32d07cf39b2a162af0f78bad8325b2fbdc89d14fd8b4e5767b74097","chaincode":"585bdc05c8f84802cbe7b9a1a07d4aa9c5fede93597a622854e9bad83a2d5b78"},"bitgoYShare":{"i":2,"j":3,"y":"483e53b72de3aa893df698d0b20b20777fb3d2716cc8483a9e9797174fd52b16","v":"e70696459e46434a2a12cc988e3ae714a61fe96da8a6764d058b849cab50d6dc","u":"eb54da28da3da22eb3d61797a02a96264be8940b7115aefbb90b9dd044db7f06","chaincode":"797348468202f1d7fede0a7851f80162b02e7da306e65075dd864b6789b9bc5b"},"userYShare":{"i":2,"j":1,"y":"85aa6462d927329418f70f6d0863cf6cf33e7da2934f935e5927f1b13062d779","v":"76cfdcbf0f769f21c64e0faf0072ebccbcc3aaa844522336af27f8e50ed7ca5f","u":"6ce814af82683423c8d8befd13f6eeeb0cd3f7274d1ebfdd5807fd2e4eaadb08","chaincode":"66e80f2bf41a5706608352d51ceb07a5aa1729cab6c6993c124d5731546ed9a1"}}',
pub: commonKeychain,
source: 'backup',
type: 'tss',
};

const dualCfg: AdvancedWalletManagerConfig = {
appMode: AppMode.ADVANCED_WALLET_MANAGER,
signingMode: SigningMode.LOCAL,
port: 0,
bind: 'localhost',
timeout: 60000,
keyProviderUrl: primaryKmsUrl,
backupKmsUrl,
httpLoggerFile: '',
tlsMode: TlsMode.DISABLED,
recoveryMode: true,
};

const dualApp = expressApp(dualCfg);
const dualAgent = request.agent(dualApp);

// User key served from primary KMS
const userKmsNock = nock(primaryKmsUrl)
.get(`/key/${commonKeychain}`)
.query({ source: 'user' })
.reply(200, mockKmsUserResponse)
.persist();

// Backup key served from backup KMS
const backupKmsNock = nock(backupKmsUrl)
.get(`/key/${commonKeychain}`)
.query({ source: 'backup' })
.reply(200, mockKmsBackupResponse)
.persist();

const input = {
commonKeychain,
unsignedSweepPrebuildTx: {
txRequests: [
{
unsignedTx: '',
signableHex:
'AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAECvoOqYkvCPusjYyhX4GdUtzSeVIcx6GkwdpSk8SkU0/cAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIQtFGO2YBsrubq15CKqJLwXG3VEF1aEs36Rao6EaJDLAQECAAAMAgAAALhJxgAAAAAA',
derivationPath: 'm/0',
},
],
},
};

const response = await dualAgent
.post(`/api/${sol}/mpc/recovery`)
.set('Authorization', `Bearer ${accessToken}`)
.send(input);

response.status.should.equal(200);
response.body.should.have.property('txHex');

userKmsNock.isDone().should.be.true();
backupKmsNock.isDone().should.be.true();
});
});

describe('ECDSA sui recovery', () => {
it('should successfully generate MPC sui transactions', async () => {
const mockKeyProviderUserResponse = {
Expand Down
81 changes: 69 additions & 12 deletions src/__tests__/api/advancedWalletManager/recoveryMpcV2.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,24 +20,22 @@ describe('recoveryMpcV2', () => {
const cosmosLikeCoin = 'tsei';
const accessToken = 'test-token';

// sinon stubs
// sinon sandbox
const sandbox = sinon.createSandbox();
let configStub: sinon.SinonStub;

// key provider nocks setup — initialized in before()
let commonKeychain!: string;
// key provider nocks setup
let userKeyShare: string;
let backupKeyShare: string;
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');
userKeyShare = userShare.getKeyShare().toString('base64');
backupKeyShare = backupShare.getKeyShare().toString('base64');
commonKeychain = DklsTypes.getCommonKeychain(userShare.getKeyShare());

mockKeyProviderUserResponse = {
Expand All @@ -60,6 +58,10 @@ describe('recoveryMpcV2', () => {
pub: commonKeychain,
};

// nock config
nock.disableNetConnect();
nock.enableNetConnect('127.0.0.1');

// app config
cfg = {
appMode: AppMode.ADVANCED_WALLET_MANAGER,
Expand All @@ -74,7 +76,7 @@ describe('recoveryMpcV2', () => {
recoveryMode: true,
};

configStub = sinon.stub(configModule, 'initConfig').returns(cfg);
configStub = sandbox.stub(configModule, 'initConfig').returns(cfg);

// app setup
app = advancedWalletManagerApp(cfg);
Expand All @@ -86,7 +88,7 @@ describe('recoveryMpcV2', () => {
});

after(() => {
configStub.restore();
sandbox.restore();
});

// happy path test
Expand Down Expand Up @@ -139,6 +141,61 @@ describe('recoveryMpcV2', () => {
backupKeyProviderNock.isDone().should.be.true();
});

it('should route backup key retrieval to backup KMS when configured', async () => {
const kmsUrl = 'http://kms.invalid';
const backupKmsUrl = 'http://backup-kms.invalid';

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

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

// Reconfigure app with backup KMS URL
const dualCfg: AdvancedWalletManagerConfig = {
...cfg,
keyProviderUrl: kmsUrl,
backupKmsUrl,
};
configStub.returns(dualCfg);
const dualApp = advancedWalletManagerApp(dualCfg);
const dualAgent = request.agent(dualApp);

// User key served from primary KMS
const userKmsNock = nock(kmsUrl)
.get(`/key/${input.pub}`)
.query({ source: 'user' })
.reply(200, mockKmsUserResponse)
.persist();

// Backup key served from backup KMS
const backupKmsNock = nock(backupKmsUrl)
.get(`/key/${input.pub}`)
.query({ source: 'backup' })
.reply(200, mockKmsBackupResponse)
.persist();

const response = await dualAgent
.post(`/api/${ethLikeCoin}/mpcv2/recovery`)
.set('Authorization', `Bearer ${accessToken}`)
.send(input);

response.status.should.equal(200);
response.body.should.have.property('txHex');
response.body.should.have.property('stringifiedSignature');

userKmsNock.isDone().should.be.true();
backupKmsNock.isDone().should.be.true();
});

// failure test case
it('should throw 400 Bad Request if failed to construct eth transaction from message hex', async () => {
const input = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ describe('recoveryMultisigTransaction', () => {
const coin = 'hteth';
const accessToken = 'test-token';

// sinon stubs
// sinon sandbox
const sandbox = sinon.createSandbox();
let configStub: sinon.SinonStub;

before(() => {
Expand All @@ -44,7 +45,7 @@ describe('recoveryMultisigTransaction', () => {
recoveryMode: true,
};

configStub = sinon.stub(configModule, 'initConfig').returns(cfg);
configStub = sandbox.stub(configModule, 'initConfig').returns(cfg);

// app setup
app = advancedWalletManagerApp(cfg);
Expand All @@ -56,7 +57,7 @@ describe('recoveryMultisigTransaction', () => {
});

after(() => {
configStub.restore();
sandbox.restore();
});

it('should generate a successful txHex from unsigned sweep prebuild data', async () => {
Expand Down Expand Up @@ -106,6 +107,67 @@ describe('recoveryMultisigTransaction', () => {
keyProviderNockBackup.done();
});

it('should route backup key retrieval to backup KMS when configured', async () => {
const kmsUrl = 'http://kms.invalid';
const backupKmsUrl = 'http://backup-kms.invalid';
const { userPub, backupPub, walletContractAddress, userPrv, backupPrv, txHexResult } = awmData;
const unsignedSweepPrebuildTx = unsignedSweepRecJSON as unknown as any;

// Reconfigure app with backup KMS URL
const dualCfg: AdvancedWalletManagerConfig = {
...cfg,
keyProviderUrl: kmsUrl,
backupKmsUrl,
};
configStub.returns(dualCfg);
const dualApp = advancedWalletManagerApp(dualCfg);
const dualAgent = request.agent(dualApp);

const mockKmsUserResponse = {
prv: userPrv,
pub: userPub,
source: 'user',
type: 'independent',
};

const mockKmsBackupResponse = {
prv: backupPrv,
pub: backupPub,
source: 'backup',
type: 'independent',
};

// User key from primary KMS
const kmsNockUser = nock(kmsUrl)
.get(`/key/${userPub}`)
.query({ source: 'user' })
.reply(200, mockKmsUserResponse);

// Backup key from backup KMS
const kmsNockBackup = nock(backupKmsUrl)
.get(`/key/${backupPub}`)
.query({ source: 'backup' })
.reply(200, mockKmsBackupResponse);

const response = await dualAgent
.post(`/api/${coin}/multisig/recovery`)
.set('Authorization', `Bearer ${accessToken}`)
.send({
userPub,
backupPub,
apiKey: 'etherscan-api-token',
unsignedSweepPrebuildTx,
walletContractAddress,
coinSpecificParams: undefined,
});

response.status.should.equal(200);
response.body.should.have.property('txHex', txHexResult);

kmsNockUser.done();
kmsNockBackup.done();
});

it('should fail when prv keys non related to pub keys', async () => {
const { userPub, backupPub, walletContractAddress } = awmData;
const unsignedSweepPrebuildTx = unsignedSweepRecJSON as unknown as any;
Expand Down
11 changes: 5 additions & 6 deletions src/advancedWalletManager/handlers/ecdsaMPCV2Recovery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@ import {
AwmApiSpecRouteRequest,
MpcV2RecoveryResponseType,
} from '../routers/advancedWalletManagerApiSpec';
import { KeyProviderClient } from '../keyProviderClient/keyProviderClient';
import { BaseCoin, ECDSAMethodTypes } from '@bitgo-beta/sdk-core';
import { isCosmosLikeCoin, isEcdsaCoin, isEthLikeCoin } from '../../shared/coinUtils';
import { BadRequestError, NotImplementedError } from '../../shared/errors';
import logger from '../../shared/logger';
import coinFactory from '../../shared/coinFactory';
import { buildBackupKmsConfig, retrieveKeyProviderPrvKey } from './utils/utils';

async function getMessageHash(coin: BaseCoin, txHex: string): Promise<Buffer> {
const txBuffer = Buffer.from(txHex, 'hex');
Expand Down Expand Up @@ -52,11 +52,10 @@ export async function ecdsaMPCv2Recovery(
);
}

// setup clients and retreive the keys
// TODO: this needs to be segerated if the EBE instance cannot retrieve both keys
const keyProvider = new KeyProviderClient(req.config);
const { prv: userPrv } = await keyProvider.getKey({ pub, source: 'user' });
const { prv: backupPrv } = await keyProvider.getKey({ pub, source: 'backup' });
// setup clients and retrieve the keys
const backupCfg = buildBackupKmsConfig(req.config);
const userPrv = await retrieveKeyProviderPrvKey({ pub, source: 'user', cfg: req.config });
const backupPrv = await retrieveKeyProviderPrvKey({ pub, source: 'backup', cfg: backupCfg });

// construct tx builder
const txHash = await getMessageHash(coin, txHex);
Expand Down
Loading
Loading