Skip to content

Commit cfae240

Browse files
WIP: tests
1 parent b9a0d1d commit cfae240

File tree

7 files changed

+539
-21
lines changed

7 files changed

+539
-21
lines changed

src/__tests__/api/enclaved/mpcFinalize.test.ts

Lines changed: 313 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
import nock from 'nock';
2+
import 'should';
3+
import * as sinon from 'sinon';
4+
import * as express from 'express';
5+
import * as request from 'supertest';
6+
import { AppMode, EnclavedConfig, TlsMode } from '../../../shared/types';
7+
import { app as enclavedApp } from '../../../enclavedApp';
8+
9+
describe('MPC Initialize', () => {
10+
let agent: request.SuperAgentTest;
11+
let app: express.Application;
12+
let cfg: EnclavedConfig;
13+
const kmsUrl = 'https://kms.com';
14+
15+
// Mock BitGo object with encrypt method
16+
const mockBitgo = {
17+
encrypt: sinon.stub().returns('encryptedTestData'),
18+
};
19+
20+
// Sample data key response from KMS
21+
const mockDataKeyResponse = {
22+
plaintextKey:
23+
'75,212,73,155,238,206,208,243,103,70,241,121,120,187,188,212,215,169,49,49,158,151,220,182,129,163,146,206,31,176,24,114',
24+
encryptedKey:
25+
'1,2,3,0,120,222,140,157,217,111,195,208,47,200,213,217,82,189,16,171,207,16,138,46,228,224,190,138,63,132,239,80,164,8,124,105,140,1,7,221,102,148,133,184,75,102,109,103,40,227,59,0,4,66,0,0,0,126,48,124,6,9,42,134,72,134,247,13,1,7,6,160,111,48,109,2,1,0,48,104,6,9,42,134,72,134,247,13,1,7,1,48,30,6,9,96,134,72,1,101,3,4,1,46,48,17,4,12,182,95,181,221,231,6,80,219,103,86,56,83,2,1,16,128,59,214,99,174,74,198,0,141,19,136,106,211,254,68,242,173,237,13,192,176,121,74,142,141,240,161,253,119,56,144,29,201,133,58,246,2,202,166,201,161,193,29,162,12,243,174,67,27,114,208,168,214,248,170,203,214,117,49,128,218',
26+
};
27+
28+
before(() => {
29+
// app config
30+
cfg = {
31+
appMode: AppMode.ENCLAVED,
32+
port: 0, // Let OS assign a free port
33+
bind: 'localhost',
34+
timeout: 60000,
35+
httpLoggerFile: '',
36+
kmsUrl: kmsUrl,
37+
tlsMode: TlsMode.DISABLED,
38+
allowSelfSigned: true,
39+
};
40+
41+
// configStub = sinon.stub(configModule, 'initConfig').returns(cfg);
42+
43+
app = enclavedApp(cfg);
44+
agent = request.agent(app);
45+
});
46+
47+
beforeEach(() => {
48+
nock.disableNetConnect();
49+
nock.enableNetConnect('127.0.0.1');
50+
// Mock KMS service
51+
nock(kmsUrl).post('/generateDataKey').reply(200, mockDataKeyResponse);
52+
});
53+
54+
afterEach(() => {
55+
sinon.restore();
56+
nock.cleanAll();
57+
});
58+
59+
it('should successfully initialize MPC key generation for user source', async () => {
60+
// Mock request object
61+
const result = await agent.post('/api/tsol/mpc/key/initialize').send({
62+
source: 'user',
63+
bitgoGpgPub:
64+
'-----BEGIN PGP PUBLIC KEY BLOCK-----\n' +
65+
'\n' +
66+
'xk8EYqEU5hMFK4EEAAoCAwQDdbAIZrsblEXIavyg2go6p9oG0SqWTgFsdHTc\n' +
67+
'BhqdIS/WjQ8pj75q+vLqFtV9hlImYGInsIWh97fsigzB2owyzRhoc20gPGhz\n' +
68+
'bUB0ZXN0LmJpdGdvLmNvbT7ChAQTEwgAFQUCYqEU5wILCQIVCAIWAAIbAwIe\n' +
69+
'AQAhCRCJNRsIDGunexYhBHRL5D/8nRM3opQnXok1GwgMa6d7tg8A/24A9awq\n' +
70+
'SCJx7RddiUzFHcKhVvvo3R5N7bHaOGP3TP79AP0TavF2WzhUXmZSjt3IK23O\n' +
71+
'7/aknbijVeq52ghbWb1SwsJ1BBATCAAGBQJioRTnACEJEAWuA35KJgtgFiEE\n' +
72+
'ZttLPR0KcYvjgvJCBa4DfkomC2BsrwD/Z+43zOw+WpfPHxe+ypyVog5fnOKl\n' +
73+
'XwleH6zDvqUWmWkA/iaHC6ullYkSG4Mv68k6qbtgR/pms/X7rkfa0QQFJy5p\n' +
74+
'zlMEYqEU5hIFK4EEAAoCAwSsLqmfonjMF3o0nZ5JHvLpmfTA1RIVDsAEoRON\n' +
75+
'tZA6rAA23pGl6s3Iyt4/fX9Adzoh3EElOjMsgi8Aj3dFpuqiAwEIB8J4BBgT\n' +
76+
'CAAJBQJioRTnAhsMACEJEIk1GwgMa6d7FiEEdEvkP/ydEzeilCdeiTUbCAxr\n' +
77+
'p3vM7AD9GPp6HhYNEh2VVCDtFSt14Bni5FVM5icpVDo6w9ibvWAA/2Ti3Jv4\n' +
78+
'IhIxl81/wqAgqigIblrz6vjtagr9/ykXQCW3\n' +
79+
'=skCo\n' +
80+
'-----END PGP PUBLIC KEY BLOCK-----\n',
81+
});
82+
83+
// Assert the response structure
84+
result.should.have.property('statusCode', 200);
85+
result.body.should.have.property('bitgoPayload');
86+
result.body.bitgoPayload.from.should.equal('user');
87+
result.body.bitgoPayload.to.should.equal('bitgo');
88+
result.body.bitgoPayload.should.have.property('privateShare');
89+
result.body.bitgoPayload.privateShare.should.not.be.empty();
90+
});
91+
92+
it('should successfully initialize MPC key generation for backup source', async () => {
93+
// Mock request object
94+
const result = await agent.post('/api/tsol/mpc/key/initialize').send({
95+
source: 'backup',
96+
bitgoGpgPub:
97+
'-----BEGIN PGP PUBLIC KEY BLOCK-----\n' +
98+
'\n' +
99+
'xk8EYqEU5hMFK4EEAAoCAwQDdbAIZrsblEXIavyg2go6p9oG0SqWTgFsdHTc\n' +
100+
'BhqdIS/WjQ8pj75q+vLqFtV9hlImYGInsIWh97fsigzB2owyzRhoc20gPGhz\n' +
101+
'bUB0ZXN0LmJpdGdvLmNvbT7ChAQTEwgAFQUCYqEU5wILCQIVCAIWAAIbAwIe\n' +
102+
'AQAhCRCJNRsIDGunexYhBHRL5D/8nRM3opQnXok1GwgMa6d7tg8A/24A9awq\n' +
103+
'SCJx7RddiUzFHcKhVvvo3R5N7bHaOGP3TP79AP0TavF2WzhUXmZSjt3IK23O\n' +
104+
'7/aknbijVeq52ghbWb1SwsJ1BBATCAAGBQJioRTnACEJEAWuA35KJgtgFiEE\n' +
105+
'ZttLPR0KcYvjgvJCBa4DfkomC2BsrwD/Z+43zOw+WpfPHxe+ypyVog5fnOKl\n' +
106+
'XwleH6zDvqUWmWkA/iaHC6ullYkSG4Mv68k6qbtgR/pms/X7rkfa0QQFJy5p\n' +
107+
'zlMEYqEU5hIFK4EEAAoCAwSsLqmfonjMF3o0nZ5JHvLpmfTA1RIVDsAEoRON\n' +
108+
'tZA6rAA23pGl6s3Iyt4/fX9Adzoh3EElOjMsgi8Aj3dFpuqiAwEIB8J4BBgT\n' +
109+
'CAAJBQJioRTnAhsMACEJEIk1GwgMa6d7FiEEdEvkP/ydEzeilCdeiTUbCAxr\n' +
110+
'p3vM7AD9GPp6HhYNEh2VVCDtFSt14Bni5FVM5icpVDo6w9ibvWAA/2Ti3Jv4\n' +
111+
'IhIxl81/wqAgqigIblrz6vjtagr9/ykXQCW3\n' +
112+
'=skCo\n' +
113+
'-----END PGP PUBLIC KEY BLOCK-----\n',
114+
counterPartyGpgPub:
115+
'-----BEGIN PGP PUBLIC KEY BLOCK-----\n' +
116+
'\n' +
117+
'xk8EYqEU5hMFK4EEAAoCAwQDdbAIZrsblEXIavyg2go6p9oG0SqWTgFsdHTc\n' +
118+
'BhqdIS/WjQ8pj75q+vLqFtV9hlImYGInsIWh97fsigzB2owyzRhoc20gPGhz\n' +
119+
'bUB0ZXN0LmJpdGdvLmNvbT7ChAQTEwgAFQUCYqEU5wILCQIVCAIWAAIbAwIe\n' +
120+
'AQAhCRCJNRsIDGunexYhBHRL5D/8nRM3opQnXok1GwgMa6d7tg8A/24A9awq\n' +
121+
'SCJx7RddiUzFHcKhVvvo3R5N7bHaOGP3TP79AP0TavF2WzhUXmZSjt3IK23O\n' +
122+
'7/aknbijVeq52ghbWb1SwsJ1BBATCAAGBQJioRTnACEJEAWuA35KJgtgFiEE\n' +
123+
'ZttLPR0KcYvjgvJCBa4DfkomC2BsrwD/Z+43zOw+WpfPHxe+ypyVog5fnOKl\n' +
124+
'XwleH6zDvqUWmWkA/iaHC6ullYkSG4Mv68k6qbtgR/pms/X7rkfa0QQFJy5p\n' +
125+
'zlMEYqEU5hIFK4EEAAoCAwSsLqmfonjMF3o0nZ5JHvLpmfTA1RIVDsAEoRON\n' +
126+
'tZA6rAA23pGl6s3Iyt4/fX9Adzoh3EElOjMsgi8Aj3dFpuqiAwEIB8J4BBgT\n' +
127+
'CAAJBQJioRTnAhsMACEJEIk1GwgMa6d7FiEEdEvkP/ydEzeilCdeiTUbCAxr\n' +
128+
'p3vM7AD9GPp6HhYNEh2VVCDtFSt14Bni5FVM5icpVDo6w9ibvWAA/2Ti3Jv4\n' +
129+
'IhIxl81/wqAgqigIblrz6vjtagr9/ykXQCW3\n' +
130+
'=skCo\n' +
131+
'-----END PGP PUBLIC KEY BLOCK-----\n',
132+
});
133+
134+
// Assert the response structure
135+
result.should.have.property('statusCode', 200);
136+
result.body.should.have.property('bitgoPayload');
137+
result.body.bitgoPayload.from.should.equal('backup');
138+
result.body.bitgoPayload.to.should.equal('bitgo');
139+
result.body.bitgoPayload.should.have.property('privateShare');
140+
result.body.bitgoPayload.privateShare.should.not.be.empty();
141+
142+
// For backup source with counterPartyGpgPub, counterPartyKeyShare should be defined
143+
result.body.should.have.property('counterPartyKeyShare');
144+
result.body.counterPartyKeyShare.from.should.equal('backup');
145+
result.body.counterPartyKeyShare.to.should.equal('user');
146+
result.body.counterPartyKeyShare.should.have.property('privateShare');
147+
result.body.counterPartyKeyShare.privateShare.should.not.be.empty();
148+
});
149+
150+
it('should fail when backup source is missing counterPartyGpgPub', async () => {
151+
// Mock request without the required counterPartyGpgPub
152+
const result = await agent.post('/api/tsol/mpc/key/initialize').send({
153+
source: 'backup',
154+
bitgoGpgPub:
155+
'-----BEGIN PGP PUBLIC KEY BLOCK-----\n' +
156+
'\n' +
157+
'xk8EYqEU5hMFK4EEAAoCAwQDdbAIZrsblEXIavyg2go6p9oG0SqWTgFsdHTc\n' +
158+
'BhqdIS/WjQ8pj75q+vLqFtV9hlImYGInsIWh97fsigzB2owyzRhoc20gPGhz\n' +
159+
'bUB0ZXN0LmJpdGdvLmNvbT7ChAQTEwgAFQUCYqEU5wILCQIVCAIWAAIbAwIe\n' +
160+
'AQAhCRCJNRsIDGunexYhBHRL5D/8nRM3opQnXok1GwgMa6d7tg8A/24A9awq\n' +
161+
'SCJx7RddiUzFHcKhVvvo3R5N7bHaOGP3TP79AP0TavF2WzhUXmZSjt3IK23O\n' +
162+
'7/aknbijVeq52ghbWb1SwsJ1BBATCAAGBQJioRTnACEJEAWuA35KJgtgFiEE\n' +
163+
'ZttLPR0KcYvjgvJCBa4DfkomC2BsrwD/Z+43zOw+WpfPHxe+ypyVog5fnOKl\n' +
164+
'XwleH6zDvqUWmWkA/iaHC6ullYkSG4Mv68k6qbtgR/pms/X7rkfa0QQFJy5p\n' +
165+
'zlMEYqEU5hIFK4EEAAoCAwSsLqmfonjMF3o0nZ5JHvLpmfTA1RIVDsAEoRON\n' +
166+
'tZA6rAA23pGl6s3Iyt4/fX9Adzoh3EElOjMsgi8Aj3dFpuqiAwEIB8J4BBgT\n' +
167+
'CAAJBQJioRTnAhsMACEJEIk1GwgMa6d7FiEEdEvkP/ydEzeilCdeiTUbCAxr\n' +
168+
'p3vM7AD9GPp6HhYNEh2VVCDtFSt14Bni5FVM5icpVDo6w9ibvWAA/2Ti3Jv4\n' +
169+
'IhIxl81/wqAgqigIblrz6vjtagr9/ykXQCW3\n' +
170+
'=skCo\n' +
171+
'-----END PGP PUBLIC KEY BLOCK-----\n',
172+
});
173+
174+
// Expect an error
175+
result.should.have.property('statusCode', 400);
176+
result.body.should.have.property('error');
177+
result.body.error.should.equal('BadRequestError');
178+
result.body.details.should.containEql('gpgKey is required on backup key share generation');
179+
});
180+
});

src/api/enclaved/mpcFinalize.ts

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,20 @@ const debugLogger = debug('bitgo:enclavedBitGoExpress:mpcFinalize');
1414
export async function eddsaFinalize(
1515
req: EnclavedApiSpecRouteRequest<'v1.mpc.key.finalize', 'post'>,
1616
) {
17+
debugLogger('MPC Finalize request received');
18+
debugLogger('Request inputs:', {
19+
source: req.decoded.source,
20+
coin: req.decoded.coin,
21+
encryptedData: req.decoded.encryptedData,
22+
encryptedDataKey: req.decoded.encryptedDataKey,
23+
bitgoKeyChain: req.decoded.bitgoKeyChain,
24+
counterPartyGpgPub: req.decoded.counterPartyGpgPub,
25+
counterPartyKeyShare: req.decoded.counterPartyKeyShare,
26+
bitgoKeyChainKeyShares: req.decoded.bitgoKeyChain.keyShares.map((keyShare: any) =>
27+
JSON.stringify(keyShare),
28+
),
29+
});
30+
1731
// request parsing
1832
const {
1933
source,
@@ -38,13 +52,14 @@ export async function eddsaFinalize(
3852

3953
// Decrypt the encrypted payload using encryptedDataKey to retrieve the previous state of computation
4054
const decryptedDataKey = await kms.decryptDataKey({ encryptedKey: encryptedDataKey });
55+
debugLogger('Decrypted Key', decryptedDataKey);
4156
const previousState = JSON.parse(
4257
req.bitgo.decrypt({
4358
input: encryptedData,
4459
password: decryptedDataKey.plaintextKey,
4560
}),
4661
);
47-
debugLogger('Decrypted previous state:', previousState);
62+
debugLogger('Decrypted previous state:', JSON.stringify(previousState));
4863
const { sourceGpgPub, sourceGpgPrv, sourcePrivateShare } = previousState;
4964
let sourceToCounterPartyKeyShare = previousState.counterPartyKeyShare;
5065

@@ -60,6 +75,10 @@ export async function eddsaFinalize(
6075
sourceGpgPrv,
6176
);
6277

78+
debugLogger('Bitgo key share:', bitgoToSourcePrivateShare);
79+
debugLogger('Source Gpg Prv', sourceGpgPrv);
80+
debugLogger('Bitgo key share:', bitgoToSourceKeyShare.privateShare);
81+
6382
await eddsaUtils.verifyWalletSignatures(
6483
source === 'user' ? sourceGpgPub : counterPartyGpgPub,
6584
source === 'user' ? counterPartyGpgPub : sourceGpgPub,
@@ -78,11 +97,15 @@ export async function eddsaFinalize(
7897
chaincode: bitgoToSourcePrivateShare.slice(64),
7998
};
8099

100+
debugLogger('counterPartyToSourceKeyShare ', counterPartyToSourceKeyShare);
101+
81102
// TOOD: clean up, probably doign unnecessary transformations
82103
const counterPartyToSourcePrivateShare = await gpgDecrypt(
83104
counterPartyToSourceKeyShare.privateShare,
84105
sourceGpgPrv,
85106
);
107+
debugLogger('counterPartyToSourcePrivateShare ', counterPartyToSourcePrivateShare);
108+
86109
const counterPartyToSourceYShare = {
87110
i: sourceIndex, // to whom
88111
j: counterPartyIndex, // from whom
@@ -142,12 +165,14 @@ export async function eddsaFinalize(
142165
};
143166
}
144167

145-
return {
168+
const result = {
146169
combinedKey,
147170
counterpartyKeyShare: sourceToCounterPartyKeyShare,
148171
source,
149172
commonKeychain,
150173
};
174+
debugLogger('MPC Finalize successful, returning result with result:', { result });
175+
return result;
151176
} catch (e) {
152177
debugLogger(`Error: ${JSON.stringify(e)}`);
153178
if (e instanceof Error) {

src/api/enclaved/mpcInitialize.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,20 @@ import {
88
MpcInitializeRequestType,
99
} from '../../enclavedBitgoExpress/routers/enclavedApiSpec';
1010
import { gpgEncrypt } from './utils';
11+
import { BadRequestError } from '../../shared/errors';
1112

1213
const debugLogger = debug('bitgo:enclavedExpress:mpcInitialize');
1314

1415
export async function eddsaInitialize(
1516
req: EnclavedApiSpecRouteRequest<'v1.mpc.key.initialize', 'post'>,
1617
) {
18+
debugLogger('MPC Initialize request received');
19+
debugLogger('Request inputs:', { source: req.decoded.source });
20+
1721
// request parsing. counterPartyGpgPub can be undefined
1822
const { source, bitgoGpgPub, counterPartyGpgPub }: MpcInitializeRequestType = req.decoded;
1923
if (source === 'backup' && !counterPartyGpgPub) {
20-
throw new Error('gpgKey is required on backup key share generation');
24+
throw new BadRequestError('gpgKey is required on backup key share generation');
2125
}
2226

2327
// setup clients
@@ -99,17 +103,20 @@ export async function eddsaInitialize(
99103
counterPartyKeyShare: counterPartyGpgPub ? undefined : counterPartyKeyShare, // if counterPartyGpgPub is NOT gpg encrypted, store in payload to be encrypted in finalize
100104
};
101105
const { plaintextKey, encryptedKey } = await kms.generateDataKey({ keyType: 'AES-256' });
106+
debugLogger(`Got encrypted key ${encryptedKey} & plaintextKey=${plaintextKey}`);
102107
try {
103108
const encryptedPayload = req.bitgo.encrypt({
104109
input: JSON.stringify(payload),
105110
password: plaintextKey,
106111
});
107-
return {
112+
const result = {
108113
encryptedDataKey: encryptedKey,
109114
encryptedData: encryptedPayload,
110115
bitgoPayload: bitgoKeyShare,
111116
counterPartyKeyShare: counterPartyGpgPub ? counterPartyKeyShare : undefined, // if counterPartyGpgPub is encrypted, send the key share unecrypted
112117
};
118+
debugLogger('MPC Initialize successful, returning result', { result });
119+
return result;
113120
} catch (error) {
114121
debugLogger('Failed to initialize mpc key generation', error);
115122
throw error;

src/enclavedBitgoExpress/routers/enclavedApiSpec.ts

Lines changed: 6 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import { signEddsaRecoveryTransaction } from '../../api/enclaved/handlers/signEd
4040
import { isEddsaCoin } from '../../shared/coinUtils';
4141
import { MethodNotImplementedError } from '@bitgo-beta/sdk-core';
4242
import coinFactory from '../../shared/coinFactory';
43+
import { BadRequestResponse, InternalServerErrorResponse } from '../../shared/errors';
4344

4445
// Request type for /key/independent endpoint
4546
const IndependentKeyRequest = {
@@ -372,10 +373,8 @@ export const EnclavedAPiSpec = apiSpec({
372373
}),
373374
response: {
374375
200: t.type(MpcInitializeResponse),
375-
500: t.type({
376-
error: t.string,
377-
details: t.string,
378-
}),
376+
...BadRequestResponse,
377+
...InternalServerErrorResponse,
379378
},
380379
description: 'Initialize MPC for EdDSA key generation',
381380
}),
@@ -571,17 +570,9 @@ export function createKeyGenRouter(config: EnclavedConfig): WrappedRouter<typeof
571570

572571
router.post('v1.mpc.key.initialize', [
573572
responseHandler<EnclavedConfig>(async (_req) => {
574-
try {
575-
const typedReq = _req as EnclavedApiSpecRouteRequest<'v1.mpc.key.initialize', 'post'>;
576-
const response = await eddsaInitialize(typedReq);
577-
return Response.ok(response);
578-
} catch (error) {
579-
const err = error as Error;
580-
return Response.internalError({
581-
error: err.message,
582-
details: err.stack || 'No stack trace available',
583-
});
584-
}
573+
const typedReq = _req as EnclavedApiSpecRouteRequest<'v1.mpc.key.initialize', 'post'>;
574+
const response = await eddsaInitialize(typedReq);
575+
return Response.ok(response);
585576
}),
586577
]);
587578

src/shared/errors.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { optional } from '@api-ts/io-ts-http';
12
import * as t from 'io-ts';
23
/**
34
* Custom error classes for specific error types
@@ -96,6 +97,7 @@ const ErrorResponse = t.type({
9697
* Error details
9798
*/
9899
details: t.string,
100+
stack: optional(t.string),
99101
});
100102
export const BadRequestResponse = { 400: ErrorResponse };
101103
export const UnprocessableEntityResponse = { 422: ErrorResponse };

src/shared/responseHandler.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,8 +85,8 @@ export function responseHandler<T extends Config = Config>(fn: ServiceFunction<T
8585
// Log the error details for debugging
8686
logger.error(JSON.stringify(errorBody, null, 2));
8787
return res.sendEncoded(statusCode, {
88-
error: error.message,
89-
name: error.name,
88+
error: error.name,
89+
stack: error.stack,
9090
details: error.message,
9191
});
9292
}

0 commit comments

Comments
 (0)