Skip to content

Commit 8be5edb

Browse files
wip(mbe): setup flow for eddsa initialization
Ticket: WP-4758
1 parent 07d987f commit 8be5edb

File tree

6 files changed

+767
-1
lines changed

6 files changed

+767
-1
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@ coverage/
55
logs/
66
tsconfig.tsbuildinfo
77
out/
8+
.vscode/

src/__tests__/masterBitgoExpress/.wip.md

Lines changed: 299 additions & 0 deletions
Large diffs are not rendered by default.

src/__tests__/masterBitgoExpress/generateWallet.test.ts

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ describe('POST /api/:coin/wallet/generate', () => {
1212
const enclavedExpressUrl = 'http://enclaved.invalid';
1313
const bitgoApiUrl = Environments.test.uri;
1414
const coin = 'tbtc';
15+
const eddsaCoin = 'tsol';
1516
const accessToken = 'test-token';
1617

1718
before(() => {
@@ -136,6 +137,135 @@ describe('POST /api/:coin/wallet/generate', () => {
136137
bitgoAddWalletNock.done();
137138
});
138139

140+
it('should generate a TSS wallet by calling the enclaved express service', async () => {
141+
const userInitNock = nock(enclavedExpressUrl)
142+
.post(`/api/${eddsaCoin}/mpc/initialize`, {
143+
source: 'user',
144+
})
145+
.reply(200, {
146+
encryptedDataKey: 'key',
147+
encryptedData: 'data',
148+
bitgoPayload: {
149+
from: 'user',
150+
to: 'bitgo',
151+
publicShare:
152+
'dcf591bfb22f9764ed382dcb397f591bdb64c69773c6cf2902d14789a13811a0a768fb0eae38f9ebe2b047182e2a95bb49921bfec56bcd96e3075e53396c1775',
153+
privateShare:
154+
'175bdf3264662e1d13de1dacc22c0913b367f165fb15439fe687cbdc1713560ca768fb0eae38f9ebe2b047182e2a95bb49921bfec56bcd96e3075e53396c1775',
155+
privateShareProof:
156+
'-----BEGIN PGP PUBLIC KEY BLOCK-----\n\nxk8EaFRmgBMFK4EEAAoCAwTGXYL4mPPKg3u1KkPeXR9lOqqem/i3kgdgQE9P\nIZlvNdZyVcoAyrTos0Negm39jQPzssKbjNYbwmD6oBliJIWDzVUxYzU3NDY3\nNmUwNWM3Zjc0Zjg4YmM5YmEgPHVzZXItMWM1NzQ2NzZlMDVjN2Y3NGY4OGJj\nOWJhQDFjNTc0Njc2ZTA1YzdmNzRmODhiYzliYS5jb20+wowEEBMIAD4FgmhU\nZoAECwkHCAmQ6ylVI/YkWEQDFQgKBBYAAgECGQECmwMCHgEWIQRS2wpzMoJX\nVNidgnnrKVUj9iRYRAAA0kkA/R78hy0CNnUPCMMi2Co6VlYALrx+xFydb0+7\n8Yza5IF2AP93Xc9FKo8OPO5pg5uPnC6fXvsJqVne289iETTtsihaaM5TBGhU\nZoASBSuBBAAKAgME99PyPC8OyvjMb5GMLIvU3UOa8vDHDw4EJxEk9vjP1M8w\n9Uz8BlRby1wYFShcTYrl8lqBmvO9KswHXSLvwyw1QAMBCAfCeAQYEwgAKgWC\naFRmgAmQ6ylVI/YkWEQCmwwWIQRS2wpzMoJXVNidgnnrKVUj9iRYRAAASxsA\n/RbBP5LPbfcAay8osipVWf/oTzw2/tKzER0K3FfAAsImAP0c2ee+qa0Tn5nv\neezRo+XxgIoxw2gT8jYpyzJw+BKBBQ==\n=DYMw\n-----END PGP PUBLIC KEY BLOCK-----\n',
157+
vssProof: '011532df3eceab48fc91c2e17e7accea1d0dd30b8b7562a5f602afb2130ab26a',
158+
userGPGPublicKey:
159+
'-----BEGIN PGP PUBLIC KEY BLOCK-----\n\nxk8EaFRmgBMFK4EEAAoCAwTGXYL4mPPKg3u1KkPeXR9lOqqem/i3kgdgQE9P\nIZlvNdZyVcoAyrTos0Negm39jQPzssKbjNYbwmD6oBliJIWDzVUxYzU3NDY3\nNmUwNWM3Zjc0Zjg4YmM5YmEgPHVzZXItMWM1NzQ2NzZlMDVjN2Y3NGY4OGJj\nOWJhQDFjNTc0Njc2ZTA1YzdmNzRmODhiYzliYS5jb20+wowEEBMIAD4FgmhU\nZoAECwkHCAmQ6ylVI/YkWEQDFQgKBBYAAgECGQECmwMCHgEWIQRS2wpzMoJX\nVNidgnnrKVUj9iRYRAAA0kkA/R78hy0CNnUPCMMi2Co6VlYALrx+xFydb0+7\n8Yza5IF2AP93Xc9FKo8OPO5pg5uPnC6fXvsJqVne289iETTtsihaaM5TBGhU\nZoASBSuBBAAKAgME99PyPC8OyvjMb5GMLIvU3UOa8vDHDw4EJxEk9vjP1M8w\n9Uz8BlRby1wYFShcTYrl8lqBmvO9KswHXSLvwyw1QAMBCAfCeAQYEwgAKgWC\naFRmgAmQ6ylVI/YkWEQCmwwWIQRS2wpzMoJXVNidgnnrKVUj9iRYRAAASxsA\n/RbBP5LPbfcAay8osipVWf/oTzw2/tKzER0K3FfAAsImAP0c2ee+qa0Tn5nv\neezRo+XxgIoxw2gT8jYpyzJw+BKBBQ==\n=DYMw\n-----END PGP PUBLIC KEY BLOCK-----\n',
160+
},
161+
});
162+
163+
const backupInitNock = nock(enclavedExpressUrl)
164+
.post(`/api/${eddsaCoin}/mpc/initialize`, {
165+
source: 'backup',
166+
})
167+
.reply(200, {
168+
encryptedDataKey: 'key',
169+
encryptedData: 'data',
170+
bitgoPayload: {
171+
from: 'backup',
172+
to: 'bitgo',
173+
publicShare:
174+
'280b5d3b40899e6e1cac86906602ffdf76b70aefc2def7f311693aba654cca6ecdcb2be051910ebc9bcbae6ac0db3edf707498b19be0f229102ce76dd880ab9b',
175+
privateShare:
176+
'1be0dcb0b3c77bceac11ce77d83b33a5b74ff39f90485d81b2003bc55270b509cdcb2be051910ebc9bcbae6ac0db3edf707498b19be0f229102ce76dd880ab9b',
177+
privateShareProof:
178+
'-----BEGIN PGP PUBLIC KEY BLOCK-----\n\nxk8EaFRmgBMFK4EEAAoCAwQbnZsAMbrZ6LnlMT8ZjmCyq4Au+KDEMH9dndk5\nqVpZIgvHzMwZYusZtija5M/erWbg0Iutv1R1olMd9htHSScOzVViMmZlNTRl\nZTI1YzIyOWM0MzJiNzU2MWYgPHVzZXItYjJmZTU0ZWUyNWMyMjljNDMyYjc1\nNjFmQGIyZmU1NGVlMjVjMjI5YzQzMmI3NTYxZi5jb20+wowEEBMIAD4FgmhU\nZoAECwkHCAmQrNctBNmaAcADFQgKBBYAAgECGQECmwMCHgEWIQSJSRD0FwPm\nwraqiESs1y0E2ZoBwAAAqJkBAIhIhHS8i71tbe43TKYThRaOzeo73afL31UE\nbK12huloAQCrjr5GEz+4L84Nl8TcWt5yAI8UF1hi+O5rdP35UL6xKc5TBGhU\nZoASBSuBBAAKAgME+Bm/MFl4fP7CxJsannVVcZ1M+bL8X8kcl30wXaLkiqvg\nZpEunra42o4RwaQcQirsvPX9+di0P2FoFXH/n1+s1wMBCAfCeAQYEwgAKgWC\naFRmgAmQrNctBNmaAcACmwwWIQSJSRD0FwPmwraqiESs1y0E2ZoBwAAAXWoB\nAI9xw2J9mzyPGpnFiIb/qxHRzSbXsNYyPvxUU15rSKiaAP9uy61NJBs3vTT8\nzf33PkAgoxFZsEDLwAsDyOecH/Cilw==\n=0SE8\n-----END PGP PUBLIC KEY BLOCK-----\n',
179+
vssProof: 'f008212df4a14e81b8b7bca268a3b2b19d65220fb2f0b2e1c8f83e0d9286aec2',
180+
backupGPGPublicKey:
181+
'-----BEGIN PGP PUBLIC KEY BLOCK-----\n\nxk8EaFRmgBMFK4EEAAoCAwQbnZsAMbrZ6LnlMT8ZjmCyq4Au+KDEMH9dndk5\nqVpZIgvHzMwZYusZtija5M/erWbg0Iutv1R1olMd9htHSScOzVViMmZlNTRl\nZTI1YzIyOWM0MzJiNzU2MWYgPHVzZXItYjJmZTU0ZWUyNWMyMjljNDMyYjc1\nNjFmQGIyZmU1NGVlMjVjMjI5YzQzMmI3NTYxZi5jb20+wowEEBMIAD4FgmhU\nZoAECwkHCAmQrNctBNmaAcADFQgKBBYAAgECGQECmwMCHgEWIQSJSRD0FwPm\nwraqiESs1y0E2ZoBwAAAqJkBAIhIhHS8i71tbe43TKYThRaOzeo73afL31UE\nbK12huloAQCrjr5GEz+4L84Nl8TcWt5yAI8UF1hi+O5rdP35UL6xKc5TBGhU\nZoASBSuBBAAKAgME+Bm/MFl4fP7CxJsannVVcZ1M+bL8X8kcl30wXaLkiqvg\nZpEunra42o4RwaQcQirsvPX9+di0P2FoFXH/n1+s1wMBCAfCeAQYEwgAKgWC\naFRmgAmQrNctBNmaAcACmwwWIQSJSRD0FwPmwraqiESs1y0E2ZoBwAAAXWoB\nAI9xw2J9mzyPGpnFiIb/qxHRzSbXsNYyPvxUU15rSKiaAP9uy61NJBs3vTT8\nzf33PkAgoxFZsEDLwAsDyOecH/Cilw==\n=0SE8\n-----END PGP PUBLIC KEY BLOCK-----\n',
182+
},
183+
});
184+
185+
const bitgoAddKeychainNock = nock(bitgoApiUrl)
186+
.post(`/api/v2/${eddsaCoin}/key`)
187+
.reply(function (uri, requestBody) {
188+
// Verify request structure
189+
const body = requestBody as any;
190+
body.should.have.properties({
191+
keyType: 'tss',
192+
source: 'bitgo',
193+
enterprise: 'test_enterprise',
194+
});
195+
196+
// Verify key shares structure
197+
body.should.have.property('keyShares').which.is.an.Array().of.length(2);
198+
199+
// Verify user share
200+
const userShare = body.keyShares.find((s: any) => s.from === 'user' && s.to === 'bitgo');
201+
userShare.should.have.properties([
202+
'publicShare',
203+
'privateShare',
204+
'privateShareProof',
205+
'vssProof',
206+
]);
207+
userShare.publicShare.should.be.a.String().and.not.empty();
208+
userShare.privateShare.should.be.a.String().and.not.empty();
209+
userShare.privateShareProof.should.startWith('-----BEGIN PGP PUBLIC KEY BLOCK-----');
210+
userShare.vssProof.should.be.a.String().and.not.empty();
211+
212+
// Verify backup share
213+
const backupShare = body.keyShares.find(
214+
(s: any) => s.from === 'backup' && s.to === 'bitgo',
215+
);
216+
backupShare.should.have.properties([
217+
'publicShare',
218+
'privateShare',
219+
'privateShareProof',
220+
'vssProof',
221+
]);
222+
backupShare.publicShare.should.be.a.String().and.not.empty();
223+
backupShare.privateShare.should.be.a.String().and.not.empty();
224+
backupShare.privateShareProof.should.startWith('-----BEGIN PGP PUBLIC KEY BLOCK-----');
225+
backupShare.vssProof.should.be.a.String().and.not.empty();
226+
227+
// Verify GPG keys
228+
body.userGPGPublicKey.should.startWith('-----BEGIN PGP PUBLIC KEY BLOCK-----');
229+
body.backupGPGPublicKey.should.startWith('-----BEGIN PGP PUBLIC KEY BLOCK-----');
230+
231+
return [
232+
200,
233+
{
234+
id: 'bitgo-key-id',
235+
commonKeychain:
236+
'4e534a0193c6636a0727079e25601abd6c2853d63582162bc53ae69b152f0ec2c2e096583da8e7ffd36dff6131a17020727f9543001525c172c1e772900359d3',
237+
},
238+
];
239+
});
240+
241+
const response = await agent
242+
.post(`/api/${eddsaCoin}/wallet/generate`)
243+
.set('Authorization', `Bearer ${accessToken}`)
244+
.send({
245+
label: 'test_wallet',
246+
enterprise: 'test_enterprise',
247+
multisigType: 'tss',
248+
});
249+
250+
// Verify response status and structure
251+
response.status.should.equal(500); // TODO: Update to 200 when fully integrated with finalize endpoint
252+
// response.body.should.have.property('bitgoKeychain');
253+
//
254+
// // Verify BitGo keychain properties
255+
// const bitgoKeychain = response.body.bitgoKeychain;
256+
// bitgoKeychain.should.have.property('id').which.is.a.String();
257+
// bitgoKeychain.should.have.property('commonKeychain').which.is.a.String();
258+
// bitgoKeychain.id.should.equal('bitgo-key-id');
259+
// bitgoKeychain.commonKeychain.should.equal(
260+
// '4e534a0193c6636a0727079e25601abd6c2853d63582162bc53ae69b152f0ec2c2e096583da8e7ffd36dff6131a17020727f9543001525c172c1e772900359d3',
261+
// );
262+
263+
// Verify all nock mocks were called
264+
userInitNock.done();
265+
backupInitNock.done();
266+
bitgoAddKeychainNock.done();
267+
});
268+
139269
it('should fail when enclaved express client is not configured', async () => {
140270
// Create a config without enclaved express settings
141271
const invalidConfig: Partial<MasterExpressConfig> = {

src/enclavedBitgoExpress/routers/enclavedApiSpec.ts

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,72 @@ import { signMultisigTransaction } from '../../api/enclaved/signMultisigTransact
1919
import { prepareBitGo, responseHandler } from '../../shared/middleware';
2020
import { EnclavedConfig } from '../../types';
2121
import { BitGoRequest } from '../../types/request';
22+
import { NotImplementedError } from 'bitgo';
2223

2324
// Request type for /key/independent endpoint
2425
const IndependentKeyRequest = {
2526
source: t.string,
2627
seed: t.union([t.undefined, t.string]),
2728
};
2829

30+
const BitgoPayloadType = t.union([
31+
t.type({
32+
from: t.literal('user'),
33+
to: t.literal('bitgo'),
34+
publicShare: t.string,
35+
privateShare: t.string,
36+
privateShareProof: t.string,
37+
vssProof: t.string,
38+
userGPGPublicKey: t.string,
39+
}),
40+
t.type({
41+
from: t.literal('backup'),
42+
to: t.literal('bitgo'),
43+
publicShare: t.string,
44+
privateShare: t.string,
45+
privateShareProof: t.string,
46+
vssProof: t.string,
47+
backupGPGPublicKey: t.string,
48+
}),
49+
]);
50+
51+
export const InitEddsaKeyGenerationRequest = {
52+
source: t.union([t.literal('user'), t.literal('backup')]),
53+
};
54+
55+
export const InitEddsaKeyGenerationResponse = t.type({
56+
encryptedDataKey: t.string,
57+
encryptedData: t.string,
58+
bitgoPayload: BitgoPayloadType,
59+
});
60+
61+
export type InitEddsaKeyGenerationResponse = t.TypeOf<typeof InitEddsaKeyGenerationResponse>;
62+
63+
// Types for /mpc/finalize endpoint
64+
const BitGoKeychainType = t.type({
65+
id: t.string,
66+
source: t.literal('bitgo'),
67+
type: t.literal('tss'),
68+
commonKeychain: t.string,
69+
verifiedVssProof: t.boolean,
70+
isBitGo: t.boolean,
71+
isTrust: t.boolean,
72+
hsmType: t.string,
73+
});
74+
75+
const FinalizeKeyGenerationRequest = {
76+
encryptedDataKey: t.string,
77+
encryptedData: t.string,
78+
bitGoKeychain: BitGoKeychainType,
79+
source: t.union([t.literal('user'), t.literal('backup')]),
80+
};
81+
82+
const FinalizeKeyGenerationResponse = t.type({
83+
commonKeychain: t.string,
84+
enclavedExpressKeyId: t.string,
85+
source: t.union([t.literal('user'), t.literal('backup')]),
86+
});
87+
2988
// Response type for /key/independent endpoint
3089
const IndependentKeyResponse: HttpResponse = {
3190
// TODO: Define proper response type
@@ -123,6 +182,46 @@ export const EnclavedAPiSpec = apiSpec({
123182
description: 'Generate an independent key',
124183
}),
125184
},
185+
'v1.key.mpc.init': {
186+
post: httpRoute({
187+
method: 'POST',
188+
path: '/api/{coin}/mpc/initialize',
189+
request: httpRequest({
190+
params: {
191+
coin: t.string,
192+
},
193+
body: InitEddsaKeyGenerationRequest,
194+
}),
195+
response: {
196+
200: InitEddsaKeyGenerationResponse,
197+
500: t.type({
198+
error: t.string,
199+
details: t.string,
200+
}),
201+
},
202+
description: 'Initialize Eddsa key generation',
203+
}),
204+
},
205+
'v1.mpc.finalize': {
206+
post: httpRoute({
207+
method: 'POST',
208+
path: '/api/{coin}/mpc/finalize',
209+
request: httpRequest({
210+
params: {
211+
coin: t.string,
212+
},
213+
body: FinalizeKeyGenerationRequest,
214+
}),
215+
response: {
216+
200: FinalizeKeyGenerationResponse,
217+
500: t.type({
218+
error: t.string,
219+
details: t.string,
220+
}),
221+
},
222+
description: 'Finalize key generation and confirm commonKeychain',
223+
}),
224+
},
126225
});
127226

128227
export type EnclavedApiSpecRouteHandler<
@@ -170,5 +269,17 @@ export function createKeyGenRouter(config: EnclavedConfig): WrappedRouter<typeof
170269
}),
171270
]);
172271

272+
router.post('v1.key.mpc.init', [
273+
responseHandler<EnclavedConfig>(async (_req) => {
274+
throw new NotImplementedError('MPC key generation is not implemented yet');
275+
}),
276+
]);
277+
278+
router.post('v1.mpc.finalize', [
279+
responseHandler<EnclavedConfig>(async (_req) => {
280+
throw new NotImplementedError('MPC key finalization is not implemented yet');
281+
}),
282+
]);
283+
173284
return router;
174285
}

src/masterBitgoExpress/enclavedExpressClient.ts

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,32 @@ import { MasterExpressConfig } from '../types';
1010
import { TlsMode } from '../types';
1111
import { EnclavedApiSpec } from '../enclavedBitgoExpress/routers';
1212
import { PingResponseType, VersionResponseType } from '../types/health';
13+
import { InitEddsaKeyGenerationResponse } from '../enclavedBitgoExpress/routers/enclavedApiSpec';
1314

1415
const debugLogger = debug('bitgo:express:enclavedExpressClient');
1516

17+
interface InitMpcKeyGenerationParams {
18+
source: 'user' | 'backup';
19+
coin?: string;
20+
}
21+
22+
interface FinalizeMpcKeyGenerationParams {
23+
source: 'user' | 'backup';
24+
coin?: string;
25+
encryptedDataKey: string;
26+
encryptedData: string;
27+
bitGoKeychain: {
28+
id: string;
29+
source: 'bitgo';
30+
type: 'tss';
31+
commonKeychain: string;
32+
verifiedVssProof: boolean;
33+
isBitGo: boolean;
34+
isTrust: boolean;
35+
hsmType: string;
36+
};
37+
}
38+
1639
interface CreateIndependentKeychainParams {
1740
source: 'user' | 'backup';
1841
coin?: string;
@@ -229,6 +252,67 @@ export class EnclavedExpressClient {
229252
throw err;
230253
}
231254
}
255+
256+
/**
257+
* Initialize MPC key generation for a given source and coin
258+
*/
259+
async initMpcKeyGeneration(
260+
params: InitMpcKeyGenerationParams,
261+
): Promise<InitEddsaKeyGenerationResponse> {
262+
if (!this.coin) {
263+
throw new Error('Coin must be specified to initialize MPC key generation');
264+
}
265+
266+
try {
267+
debugLogger('Initializing MPC key generation for coin: %s', this.coin);
268+
let request = this.apiClient['v1.key.mpc.init'].post({
269+
coin: this.coin,
270+
source: params.source,
271+
});
272+
273+
if (this.tlsMode === TlsMode.MTLS) {
274+
request = request.agent(this.createHttpsAgent());
275+
}
276+
277+
const response = await request.decodeExpecting(200);
278+
return response.body;
279+
} catch (error) {
280+
const err = error as Error;
281+
debugLogger('Failed to initialize MPC key generation: %s', err.message);
282+
throw err;
283+
}
284+
}
285+
286+
/**
287+
* Finalize MPC key generation for a given source and coin
288+
*/
289+
async finalizeMpcKeyGeneration(params: FinalizeMpcKeyGenerationParams): Promise<any> {
290+
if (!this.coin) {
291+
throw new Error('Coin must be specified to finalize MPC key generation');
292+
}
293+
294+
try {
295+
debugLogger('Finalizing MPC key generation for coin: %s', this.coin);
296+
let request = this.apiClient['v1.mpc.finalize'].post({
297+
coin: this.coin,
298+
source: params.source,
299+
encryptedDataKey: params.encryptedDataKey,
300+
encryptedData: params.encryptedData,
301+
bitGoKeychain: params.bitGoKeychain,
302+
});
303+
304+
if (this.tlsMode === TlsMode.MTLS) {
305+
request = request.agent(this.createHttpsAgent());
306+
}
307+
308+
const response = await request.decodeExpecting(200);
309+
return response.body;
310+
} catch (error) {
311+
const err = error as Error;
312+
debugLogger('Failed to finalize MPC key generation: %s', err.message);
313+
throw err;
314+
}
315+
}
232316
}
233317

234318
/**

0 commit comments

Comments
 (0)