Skip to content

Commit decefd1

Browse files
feat(awm): backup key provider in recoveries
Ticket: WCN-363
1 parent fda6f15 commit decefd1

10 files changed

Lines changed: 316 additions & 26 deletions

File tree

src/__tests__/api/advancedWalletManager/postMpcV2Key.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ describe('postMpcV2Key', () => {
2121
const coin = 'hteth';
2222
const accessToken = 'test-token';
2323

24-
// sinon stubs
25-
let configStub: sinon.SinonStub;
24+
// sinon sandbox
25+
const sandbox = sinon.createSandbox();
2626

2727
before(() => {
2828
// nock config
@@ -42,7 +42,7 @@ describe('postMpcV2Key', () => {
4242
clientCertAllowSelfSigned: true,
4343
};
4444

45-
configStub = sinon.stub(configModule, 'initConfig').returns(cfg);
45+
sandbox.stub(configModule, 'initConfig').returns(cfg);
4646

4747
// app setup
4848
app = advancedWalletManagerApp(cfg);
@@ -93,7 +93,7 @@ describe('postMpcV2Key', () => {
9393
});
9494

9595
after(() => {
96-
configStub.restore();
96+
sandbox.restore();
9797
});
9898

9999
it('should be able to create a new MPC V2 wallet', async () => {

src/__tests__/api/advancedWalletManager/recoveryMpc.test.ts

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,85 @@ describe('recoveryMpc', () => {
142142
});
143143
});
144144

145+
describe('EdDSA dual-KMS recovery', () => {
146+
it('should route backup key retrieval to backup KMS when configured', async () => {
147+
const primaryKmsUrl = 'http://primary-kms.invalid';
148+
const backupKmsUrl = 'http://backup-kms.invalid';
149+
150+
const commonKeychain =
151+
'b6f5fb808f538a32735a89609e98fab75690a2c79b26f50a54c4cbf0fbca287138b733783f1590e12b4916ef0f6053b22044860117274bda44bd5d711855f174';
152+
153+
const mockKmsUserResponse = {
154+
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"}}',
155+
pub: commonKeychain,
156+
source: 'user',
157+
type: 'tss',
158+
};
159+
160+
const mockKmsBackupResponse = {
161+
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"}}',
162+
pub: commonKeychain,
163+
source: 'backup',
164+
type: 'tss',
165+
};
166+
167+
const dualCfg: AdvancedWalletManagerConfig = {
168+
appMode: AppMode.ADVANCED_WALLET_MANAGER,
169+
signingMode: SigningMode.LOCAL,
170+
port: 0,
171+
bind: 'localhost',
172+
timeout: 60000,
173+
keyProviderUrl: primaryKmsUrl,
174+
backupKmsUrl,
175+
httpLoggerFile: '',
176+
tlsMode: TlsMode.DISABLED,
177+
recoveryMode: true,
178+
};
179+
180+
const dualApp = expressApp(dualCfg);
181+
const dualAgent = request.agent(dualApp);
182+
183+
// User key served from primary KMS
184+
const userKmsNock = nock(primaryKmsUrl)
185+
.get(`/key/${commonKeychain}`)
186+
.query({ source: 'user' })
187+
.reply(200, mockKmsUserResponse)
188+
.persist();
189+
190+
// Backup key served from backup KMS
191+
const backupKmsNock = nock(backupKmsUrl)
192+
.get(`/key/${commonKeychain}`)
193+
.query({ source: 'backup' })
194+
.reply(200, mockKmsBackupResponse)
195+
.persist();
196+
197+
const input = {
198+
commonKeychain,
199+
unsignedSweepPrebuildTx: {
200+
txRequests: [
201+
{
202+
unsignedTx: '',
203+
signableHex:
204+
'AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAECvoOqYkvCPusjYyhX4GdUtzSeVIcx6GkwdpSk8SkU0/cAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIQtFGO2YBsrubq15CKqJLwXG3VEF1aEs36Rao6EaJDLAQECAAAMAgAAALhJxgAAAAAA',
205+
derivationPath: 'm/0',
206+
},
207+
],
208+
},
209+
};
210+
211+
const response = await dualAgent
212+
.post(`/api/${sol}/mpc/recovery`)
213+
.set('Authorization', `Bearer ${accessToken}`)
214+
.send(input);
215+
216+
response.status.should.equal(200);
217+
response.body.should.have.property('txHex');
218+
219+
userKmsNock.isDone().should.be.true();
220+
backupKmsNock.isDone().should.be.true();
221+
});
222+
});
223+
145224
describe('ECDSA sui recovery', () => {
146225
it('should successfully generate MPC sui transactions', async () => {
147226
const mockKeyProviderUserResponse = {

src/__tests__/api/advancedWalletManager/recoveryMpcV2.test.ts

Lines changed: 69 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,24 +20,22 @@ describe('recoveryMpcV2', () => {
2020
const cosmosLikeCoin = 'tsei';
2121
const accessToken = 'test-token';
2222

23-
// sinon stubs
23+
// sinon sandbox
24+
const sandbox = sinon.createSandbox();
2425
let configStub: sinon.SinonStub;
2526

26-
// key provider nocks setup — initialized in before()
27-
let commonKeychain!: string;
27+
// key provider nocks setup
28+
let userKeyShare: string;
29+
let backupKeyShare: string;
30+
let commonKeychain: string;
2831
let mockKeyProviderUserResponse: { prv: string; pub: string; source: string; type: string };
2932
let mockKeyProviderBackupResponse: { prv: string; pub: string; source: string; type: string };
3033
let input: { txHex: string; pub: string };
3134

3235
before(async () => {
33-
// nock config
34-
nock.disableNetConnect();
35-
nock.enableNetConnect('127.0.0.1');
36-
37-
// generate DKG key shares
3836
const [userShare, backupShare] = await DklsUtils.generateDKGKeyShares();
39-
const userKeyShare = userShare.getKeyShare().toString('base64');
40-
const backupKeyShare = backupShare.getKeyShare().toString('base64');
37+
userKeyShare = userShare.getKeyShare().toString('base64');
38+
backupKeyShare = backupShare.getKeyShare().toString('base64');
4139
commonKeychain = DklsTypes.getCommonKeychain(userShare.getKeyShare());
4240

4341
mockKeyProviderUserResponse = {
@@ -60,6 +58,10 @@ describe('recoveryMpcV2', () => {
6058
pub: commonKeychain,
6159
};
6260

61+
// nock config
62+
nock.disableNetConnect();
63+
nock.enableNetConnect('127.0.0.1');
64+
6365
// app config
6466
cfg = {
6567
appMode: AppMode.ADVANCED_WALLET_MANAGER,
@@ -74,7 +76,7 @@ describe('recoveryMpcV2', () => {
7476
recoveryMode: true,
7577
};
7678

77-
configStub = sinon.stub(configModule, 'initConfig').returns(cfg);
79+
configStub = sandbox.stub(configModule, 'initConfig').returns(cfg);
7880

7981
// app setup
8082
app = advancedWalletManagerApp(cfg);
@@ -86,7 +88,7 @@ describe('recoveryMpcV2', () => {
8688
});
8789

8890
after(() => {
89-
configStub.restore();
91+
sandbox.restore();
9092
});
9193

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

144+
it('should route backup key retrieval to backup KMS when configured', async () => {
145+
const kmsUrl = 'http://kms.invalid';
146+
const backupKmsUrl = 'http://backup-kms.invalid';
147+
148+
const mockKmsUserResponse = {
149+
prv: JSON.stringify(userKeyShare),
150+
pub: commonKeychain,
151+
source: 'user',
152+
type: 'tss',
153+
};
154+
155+
const mockKmsBackupResponse = {
156+
prv: JSON.stringify(backupKeyShare),
157+
pub: commonKeychain,
158+
source: 'backup',
159+
type: 'tss',
160+
};
161+
162+
// Reconfigure app with backup KMS URL
163+
const dualCfg: AdvancedWalletManagerConfig = {
164+
...cfg,
165+
keyProviderUrl: kmsUrl,
166+
backupKmsUrl,
167+
};
168+
configStub.returns(dualCfg);
169+
const dualApp = advancedWalletManagerApp(dualCfg);
170+
const dualAgent = request.agent(dualApp);
171+
172+
// User key served from primary KMS
173+
const userKmsNock = nock(kmsUrl)
174+
.get(`/key/${input.pub}`)
175+
.query({ source: 'user' })
176+
.reply(200, mockKmsUserResponse)
177+
.persist();
178+
179+
// Backup key served from backup KMS
180+
const backupKmsNock = nock(backupKmsUrl)
181+
.get(`/key/${input.pub}`)
182+
.query({ source: 'backup' })
183+
.reply(200, mockKmsBackupResponse)
184+
.persist();
185+
186+
const response = await dualAgent
187+
.post(`/api/${ethLikeCoin}/mpcv2/recovery`)
188+
.set('Authorization', `Bearer ${accessToken}`)
189+
.send(input);
190+
191+
response.status.should.equal(200);
192+
response.body.should.have.property('txHex');
193+
response.body.should.have.property('stringifiedSignature');
194+
195+
userKmsNock.isDone().should.be.true();
196+
backupKmsNock.isDone().should.be.true();
197+
});
198+
142199
// failure test case
143200
it('should throw 400 Bad Request if failed to construct eth transaction from message hex', async () => {
144201
const input = {

src/__tests__/api/advancedWalletManager/recoveryMusigEth.test.ts

Lines changed: 65 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ describe('recoveryMultisigTransaction', () => {
2222
const coin = 'hteth';
2323
const accessToken = 'test-token';
2424

25-
// sinon stubs
25+
// sinon sandbox
26+
const sandbox = sinon.createSandbox();
2627
let configStub: sinon.SinonStub;
2728

2829
before(() => {
@@ -44,7 +45,7 @@ describe('recoveryMultisigTransaction', () => {
4445
recoveryMode: true,
4546
};
4647

47-
configStub = sinon.stub(configModule, 'initConfig').returns(cfg);
48+
configStub = sandbox.stub(configModule, 'initConfig').returns(cfg);
4849

4950
// app setup
5051
app = advancedWalletManagerApp(cfg);
@@ -56,7 +57,7 @@ describe('recoveryMultisigTransaction', () => {
5657
});
5758

5859
after(() => {
59-
configStub.restore();
60+
sandbox.restore();
6061
});
6162

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

110+
it('should route backup key retrieval to backup KMS when configured', async () => {
111+
const kmsUrl = 'http://kms.invalid';
112+
const backupKmsUrl = 'http://backup-kms.invalid';
113+
const { userPub, backupPub, walletContractAddress, userPrv, backupPrv, txHexResult } = awmData;
114+
const unsignedSweepPrebuildTx = unsignedSweepRecJSON as unknown as any;
115+
116+
// Reconfigure app with backup KMS URL
117+
const dualCfg: AdvancedWalletManagerConfig = {
118+
...cfg,
119+
keyProviderUrl: kmsUrl,
120+
backupKmsUrl,
121+
};
122+
configStub.returns(dualCfg);
123+
const dualApp = advancedWalletManagerApp(dualCfg);
124+
const dualAgent = request.agent(dualApp);
125+
126+
const mockKmsUserResponse = {
127+
prv: userPrv,
128+
pub: userPub,
129+
source: 'user',
130+
type: 'independent',
131+
};
132+
133+
const mockKmsBackupResponse = {
134+
prv: backupPrv,
135+
pub: backupPub,
136+
source: 'backup',
137+
type: 'independent',
138+
};
139+
140+
// User key from primary KMS
141+
const kmsNockUser = nock(kmsUrl)
142+
.get(`/key/${userPub}`)
143+
.query({ source: 'user' })
144+
.reply(200, mockKmsUserResponse);
145+
146+
// Backup key from backup KMS
147+
const kmsNockBackup = nock(backupKmsUrl)
148+
.get(`/key/${backupPub}`)
149+
.query({ source: 'backup' })
150+
.reply(200, mockKmsBackupResponse);
151+
152+
const response = await dualAgent
153+
.post(`/api/${coin}/multisig/recovery`)
154+
.set('Authorization', `Bearer ${accessToken}`)
155+
.send({
156+
userPub,
157+
backupPub,
158+
apiKey: 'etherscan-api-token',
159+
unsignedSweepPrebuildTx,
160+
walletContractAddress,
161+
coinSpecificParams: undefined,
162+
});
163+
164+
response.status.should.equal(200);
165+
response.body.should.have.property('txHex', txHexResult);
166+
167+
kmsNockUser.done();
168+
kmsNockBackup.done();
169+
});
170+
109171
it('should fail when prv keys non related to pub keys', async () => {
110172
const { userPub, backupPub, walletContractAddress } = awmData;
111173
const unsignedSweepPrebuildTx = unsignedSweepRecJSON as unknown as any;

src/advancedWalletManager/handlers/ecdsaMPCV2Recovery.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,12 @@ import {
33
AwmApiSpecRouteRequest,
44
MpcV2RecoveryResponseType,
55
} from '../routers/advancedWalletManagerApiSpec';
6-
import { KeyProviderClient } from '../keyProviderClient/keyProviderClient';
76
import { BaseCoin, ECDSAMethodTypes } from '@bitgo-beta/sdk-core';
87
import { isCosmosLikeCoin, isEcdsaCoin, isEthLikeCoin } from '../../shared/coinUtils';
98
import { BadRequestError, NotImplementedError } from '../../shared/errors';
109
import logger from '../../shared/logger';
1110
import coinFactory from '../../shared/coinFactory';
11+
import { buildBackupKmsConfig, retrieveKeyProviderPrvKey } from './utils/utils';
1212

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

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

6160
// construct tx builder
6261
const txHash = await getMessageHash(coin, txHex);

0 commit comments

Comments
 (0)