Skip to content

Commit b5bd309

Browse files
feat(awm): fix recovery handlers to support dual key provider
Ticket: WCN-363
1 parent 020ec0d commit b5bd309

9 files changed

Lines changed: 269 additions & 40 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: 91 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import * as sinon from 'sinon';
99
import * as configModule from '../../../initConfig';
1010
import { DklsTypes, DklsUtils } from '@bitgo-beta/sdk-lib-mpc';
1111

12-
describe('recoveryMpcV2', async () => {
12+
describe('recoveryMpcV2', () => {
1313
let cfg: AdvancedWalletManagerConfig;
1414
let app: express.Application;
1515
let agent: request.SuperAgentTest;
@@ -20,35 +20,44 @@ describe('recoveryMpcV2', async () => {
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

2627
// key provider nocks setup
27-
const [userShare, backupShare] = await DklsUtils.generateDKGKeyShares();
28-
const userKeyShare = userShare.getKeyShare().toString('base64');
29-
const backupKeyShare = backupShare.getKeyShare().toString('base64');
30-
const commonKeychain = DklsTypes.getCommonKeychain(userShare.getKeyShare());
31-
32-
const mockKeyProviderUserResponse = {
33-
prv: JSON.stringify(userKeyShare),
34-
pub: commonKeychain,
35-
source: 'user',
36-
type: 'tss',
37-
};
38-
39-
const mockKeyProviderBackupResponse = {
40-
prv: JSON.stringify(backupKeyShare),
41-
pub: commonKeychain,
42-
source: 'backup',
43-
type: 'tss',
44-
};
45-
const input = {
46-
txHex:
47-
'02f6824268018502540be4008504a817c80083030d409443442e403d64d29c4f64065d0c1a0e8edc03d6c88801550f7dca700000823078c0',
48-
pub: commonKeychain,
49-
};
28+
let userKeyShare: string;
29+
let backupKeyShare: string;
30+
let commonKeychain: string;
31+
let mockKeyProviderUserResponse: { prv: string; pub: string; source: string; type: string };
32+
let mockKeyProviderBackupResponse: { prv: string; pub: string; source: string; type: string };
33+
let input: { txHex: string; pub: string };
5034

5135
before(async () => {
36+
const [userShare, backupShare] = await DklsUtils.generateDKGKeyShares();
37+
userKeyShare = userShare.getKeyShare().toString('base64');
38+
backupKeyShare = backupShare.getKeyShare().toString('base64');
39+
commonKeychain = DklsTypes.getCommonKeychain(userShare.getKeyShare());
40+
41+
mockKeyProviderUserResponse = {
42+
prv: JSON.stringify(userKeyShare),
43+
pub: commonKeychain,
44+
source: 'user',
45+
type: 'tss',
46+
};
47+
48+
mockKeyProviderBackupResponse = {
49+
prv: JSON.stringify(backupKeyShare),
50+
pub: commonKeychain,
51+
source: 'backup',
52+
type: 'tss',
53+
};
54+
55+
input = {
56+
txHex:
57+
'02f6824268018502540be4008504a817c80083030d409443442e403d64d29c4f64065d0c1a0e8edc03d6c88801550f7dca700000823078c0',
58+
pub: commonKeychain,
59+
};
60+
5261
// nock config
5362
nock.disableNetConnect();
5463
nock.enableNetConnect('127.0.0.1');
@@ -67,7 +76,7 @@ describe('recoveryMpcV2', async () => {
6776
recoveryMode: true,
6877
};
6978

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

7281
// app setup
7382
app = advancedWalletManagerApp(cfg);
@@ -79,7 +88,7 @@ describe('recoveryMpcV2', async () => {
7988
});
8089

8190
after(() => {
82-
configStub.restore();
91+
sandbox.restore();
8392
});
8493

8594
// happy path test
@@ -132,6 +141,61 @@ describe('recoveryMpcV2', async () => {
132141
backupKeyProviderNock.isDone().should.be.true();
133142
});
134143

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+
135199
// failure test case
136200
it('should throw 400 Bad Request if failed to construct eth transaction from message hex', async () => {
137201
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: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -52,11 +52,14 @@ 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 userKeyProvider = new KeyProviderClient(req.config);
57+
const backupCfg = req.config.backupKmsUrl
58+
? { ...req.config, keyProviderUrl: req.config.backupKmsUrl }
59+
: req.config;
60+
const backupKeyProvider = new KeyProviderClient(backupCfg);
61+
const { prv: userPrv } = await userKeyProvider.getKey({ pub, source: 'user' });
62+
const { prv: backupPrv } = await backupKeyProvider.getKey({ pub, source: 'backup' });
6063

6164
// construct tx builder
6265
const txHash = await getMessageHash(coin, txHex);

src/advancedWalletManager/handlers/utils/utils.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ export async function retrieveKeyProviderPrvKey({
1616
source: string;
1717
cfg: AdvancedWalletManagerConfig;
1818
}): Promise<string> {
19-
const keyProvider = new KeyProviderClient(cfg);
19+
const effectiveCfg =
20+
source === 'backup' && cfg.backupKmsUrl ? { ...cfg, keyProviderUrl: cfg.backupKmsUrl } : cfg;
21+
const keyProvider = new KeyProviderClient(effectiveCfg);
2022
// Retrieve the private key from key provider
2123
let prv: string;
2224
try {

0 commit comments

Comments
 (0)