Skip to content

Commit 9485b23

Browse files
committed
feat: implement EdDSA MPS DKG key gen orchestration
TICKET: WCI-5
1 parent 8ba67c9 commit 9485b23

8 files changed

Lines changed: 700 additions & 0 deletions

File tree

Lines changed: 311 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,311 @@
1+
import * as assert from 'assert';
2+
import nock = require('nock');
3+
import * as openpgp from 'openpgp';
4+
5+
import { EddsaMPCv2KeyGenRound1Request, EddsaMPCv2KeyGenRound2Request } from '@bitgo/public-types';
6+
import { TestableBG, TestBitGo } from '@bitgo/sdk-test';
7+
import { AddKeychainOptions, BaseCoin, common, ECDSAUtils, EDDSAUtils, Keychain, Wallet } from '@bitgo/sdk-core';
8+
import { EddsaMPSDkg, MPSComms, MPSTypes } from '@bitgo/sdk-lib-mpc';
9+
import { BitGo, BitgoGPGPublicKey } from '../../../../../../src';
10+
11+
const MPCv2PartiesEnum = ECDSAUtils.MPCv2PartiesEnum;
12+
13+
describe('TSS EdDSA MPCv2 Utils:', async function () {
14+
const coinName = 'sol';
15+
const walletId = '5b34252f1bf349930e34020a00000000';
16+
const enterpriseId = '6449153a6f6bc20006d66771cdbe15d3';
17+
18+
let bgUrl: string;
19+
let tssUtils: EDDSAUtils.EddsaMPCv2Utils;
20+
let wallet: Wallet;
21+
let bitgo: TestableBG & BitGo;
22+
let baseCoin: BaseCoin;
23+
24+
let bitgoGpgKeyPair: openpgp.SerializedKeyPair<string> & { revocationCertificate: string };
25+
let bitgoPrvKeyObj: openpgp.PrivateKey;
26+
let constants: { mpc: { bitgoPublicKey: string; bitgoMPCv2PublicKey: string } };
27+
28+
beforeEach(async function () {
29+
nock.cleanAll();
30+
await nockGetBitgoPublicKeyBasedOnFeatureFlags(coinName, enterpriseId, bitgoGpgKeyPair);
31+
nock(bgUrl).get('/api/v1/client/constants').times(10).reply(200, { ttl: 3600, constants });
32+
});
33+
34+
before(async function () {
35+
openpgp.config.rejectCurves = new Set();
36+
37+
bitgoGpgKeyPair = await openpgp.generateKey({
38+
userIDs: [{ name: 'bitgo', email: 'bitgo@test.com' }],
39+
curve: 'ed25519',
40+
format: 'armored',
41+
});
42+
43+
bitgoPrvKeyObj = await openpgp.readPrivateKey({ armoredKey: bitgoGpgKeyPair.privateKey });
44+
45+
constants = {
46+
mpc: {
47+
bitgoPublicKey: bitgoGpgKeyPair.publicKey,
48+
bitgoMPCv2PublicKey: bitgoGpgKeyPair.publicKey,
49+
},
50+
};
51+
52+
bitgo = TestBitGo.decorate(BitGo, { env: 'mock' });
53+
bitgo.initializeTestVars();
54+
baseCoin = bitgo.coin(coinName);
55+
bgUrl = common.Environments[bitgo.getEnv()].uri;
56+
57+
const walletData = {
58+
id: walletId,
59+
enterprise: enterpriseId,
60+
coin: coinName,
61+
coinSpecific: {},
62+
multisigType: 'tss',
63+
};
64+
wallet = new Wallet(bitgo, baseCoin, walletData);
65+
tssUtils = new EDDSAUtils.EddsaMPCv2Utils(bitgo, baseCoin, wallet);
66+
});
67+
68+
after(function () {
69+
nock.cleanAll();
70+
});
71+
72+
describe('TSS key chains', async function () {
73+
it('should generate TSS MPS keys', async function () {
74+
const bitgoSession = new EddsaMPSDkg.DKG(3, 2, 2);
75+
const bitgoState: { msg2?: MPSTypes.DeserializedMessage } = {};
76+
const round1Nock = await nockMPSKeyGenRound1(bitgoSession, bitgoState, 1);
77+
const round2Nock = await nockMPSKeyGenRound2(bitgoSession, bitgoState, 1);
78+
const addKeyNock = await nockAddKeyChain(coinName, 3);
79+
80+
const params = {
81+
passphrase: 'test',
82+
enterprise: enterpriseId,
83+
originalPasscodeEncryptionCode: '123456',
84+
};
85+
86+
const { userKeychain, backupKeychain, bitgoKeychain } = await tssUtils.createKeychains(params);
87+
88+
assert.ok(round1Nock.isDone());
89+
assert.ok(round2Nock.isDone());
90+
assert.ok(addKeyNock.isDone());
91+
92+
assert.ok(userKeychain);
93+
assert.equal(userKeychain.source, 'user');
94+
assert.ok(userKeychain.commonKeychain);
95+
assert.ok(userKeychain.encryptedPrv);
96+
assert.ok(bitgo.decrypt({ input: userKeychain.encryptedPrv, password: params.passphrase }));
97+
98+
assert.ok(backupKeychain);
99+
assert.equal(backupKeychain.source, 'backup');
100+
assert.ok(backupKeychain.commonKeychain);
101+
assert.ok(backupKeychain.encryptedPrv);
102+
assert.ok(bitgo.decrypt({ input: backupKeychain.encryptedPrv, password: params.passphrase }));
103+
104+
assert.ok(bitgoKeychain);
105+
assert.equal(bitgoKeychain.source, 'bitgo');
106+
assert.ok(bitgoKeychain.commonKeychain);
107+
108+
assert.equal(userKeychain.commonKeychain, backupKeychain.commonKeychain);
109+
assert.equal(userKeychain.commonKeychain, bitgoKeychain.commonKeychain);
110+
});
111+
112+
it('should create TSS key chains', async function () {
113+
const fakeCommonKeychain = 'a'.repeat(64);
114+
115+
const nockPromises = [
116+
nockKeychain({
117+
coin: coinName,
118+
keyChain: { id: '1', pub: '1', type: 'tss', source: 'user', reducedEncryptedPrv: '' },
119+
source: 'user',
120+
}),
121+
nockKeychain({
122+
coin: coinName,
123+
keyChain: { id: '2', pub: '2', type: 'tss', source: 'backup', reducedEncryptedPrv: '' },
124+
source: 'backup',
125+
}),
126+
nockKeychain({
127+
coin: coinName,
128+
keyChain: { id: '3', pub: '3', type: 'tss', source: 'bitgo', reducedEncryptedPrv: '' },
129+
source: 'bitgo',
130+
}),
131+
];
132+
const [nockedUserKeychain, nockedBackupKeychain, nockedBitGoKeychain] = await Promise.all(nockPromises);
133+
134+
const [userKeychain, backupKeychain, bitgoKeychain] = await Promise.all([
135+
tssUtils.createParticipantKeychain(
136+
MPCv2PartiesEnum.USER,
137+
fakeCommonKeychain,
138+
Buffer.from('userPrivate'),
139+
Buffer.from('userReduced'),
140+
'passphrase'
141+
),
142+
tssUtils.createParticipantKeychain(
143+
MPCv2PartiesEnum.BACKUP,
144+
fakeCommonKeychain,
145+
Buffer.from('backupPrivate'),
146+
Buffer.from('backupReduced'),
147+
'passphrase'
148+
),
149+
tssUtils.createParticipantKeychain(MPCv2PartiesEnum.BITGO, fakeCommonKeychain),
150+
]);
151+
152+
assert.ok(userKeychain);
153+
assert.equal(bitgoKeychain.source, 'bitgo');
154+
155+
assert.equal(userKeychain.id, nockedUserKeychain.id);
156+
assert.equal(backupKeychain.id, nockedBackupKeychain.id);
157+
assert.equal(bitgoKeychain.id, nockedBitGoKeychain.id);
158+
159+
({ ...userKeychain, reducedEncryptedPrv: '' }).should.deepEqual(nockedUserKeychain);
160+
({ ...backupKeychain, reducedEncryptedPrv: '' }).should.deepEqual(nockedBackupKeychain);
161+
({ ...bitgoKeychain, reducedEncryptedPrv: '' }).should.deepEqual(nockedBitGoKeychain);
162+
});
163+
164+
it('should reject when BitGo PGP signature on round 1 response is invalid', async function () {
165+
nock(bgUrl)
166+
.post('/api/v2/mpc/generatekey', (body) => body.round === 'MPS-R1')
167+
.once()
168+
.reply(200, {
169+
sessionId: 'bad-session',
170+
bitgoMsg1: {
171+
message: Buffer.from('garbage').toString('base64'),
172+
signature: '-----BEGIN PGP SIGNATURE-----\nFAKE\n-----END PGP SIGNATURE-----',
173+
},
174+
});
175+
176+
await assert.rejects(tssUtils.createKeychains({ passphrase: 'test', enterprise: enterpriseId }));
177+
});
178+
});
179+
180+
// ---------------------------------------------------------------------------
181+
// Nock helpers
182+
// ---------------------------------------------------------------------------
183+
184+
async function nockGetBitgoPublicKeyBasedOnFeatureFlags(
185+
coin: string,
186+
enterprise: string,
187+
bitgoKeyPair: openpgp.SerializedKeyPair<string>
188+
): Promise<BitgoGPGPublicKey> {
189+
const response: BitgoGPGPublicKey = {
190+
name: 'irrelevant',
191+
publicKey: bitgoKeyPair.publicKey,
192+
mpcv2PublicKey: bitgoKeyPair.publicKey,
193+
enterpriseId: enterprise,
194+
};
195+
nock(bgUrl).get(`/api/v2/${coin}/tss/pubkey`).query({ enterpriseId: enterprise }).reply(200, response);
196+
return response;
197+
}
198+
199+
async function nockMPSKeyGenRound1(
200+
bitgoSession: EddsaMPSDkg.DKG,
201+
bitgoState: { msg2?: MPSTypes.DeserializedMessage },
202+
times = 1
203+
) {
204+
return nock(bgUrl)
205+
.post('/api/v2/mpc/generatekey', (body) => body.round === 'MPS-R1')
206+
.times(times)
207+
.reply(200, async (_uri, { payload }: { payload: EddsaMPCv2KeyGenRound1Request }) => {
208+
const { userGpgPublicKey, backupGpgPublicKey, userMsg1, backupMsg1 } = payload;
209+
210+
openpgp.config.rejectCurves = new Set();
211+
const userPubKeyObj = await openpgp.readKey({ armoredKey: userGpgPublicKey });
212+
const backupPubKeyObj = await openpgp.readKey({ armoredKey: backupGpgPublicKey });
213+
214+
const userPk = Buffer.from(
215+
((await userPubKeyObj.getEncryptionKey()).keyPacket.publicParams as { Q: Uint8Array }).Q
216+
).subarray(1);
217+
const backupPk = Buffer.from(
218+
((await backupPubKeyObj.getEncryptionKey()).keyPacket.publicParams as { Q: Uint8Array }).Q
219+
).subarray(1);
220+
const bitgoSk = Buffer.from(
221+
((await bitgoPrvKeyObj.getDecryptionKeys())[0].keyPacket.privateParams as { d: Uint8Array }).d
222+
).reverse();
223+
224+
bitgoSession.initDkg(bitgoSk, [userPk, backupPk]);
225+
const bitgoRawMsg1 = bitgoSession.getFirstMessage();
226+
227+
await MPSComms.verifyMpsMessage(userMsg1, userPubKeyObj);
228+
await MPSComms.verifyMpsMessage(backupMsg1, backupPubKeyObj);
229+
230+
// Process all 3 round-1 messages (including BitGo's own) to advance state and produce bitgoMsg2
231+
const userDeserMsg1: MPSTypes.DeserializedMessage = {
232+
from: 0,
233+
payload: new Uint8Array(Buffer.from(userMsg1.message, 'base64')),
234+
};
235+
const backupDeserMsg1: MPSTypes.DeserializedMessage = {
236+
from: 1,
237+
payload: new Uint8Array(Buffer.from(backupMsg1.message, 'base64')),
238+
};
239+
const [bitgoRawMsg2] = bitgoSession.handleIncomingMessages([userDeserMsg1, backupDeserMsg1, bitgoRawMsg1]);
240+
bitgoState.msg2 = bitgoRawMsg2;
241+
242+
return {
243+
sessionId: 'test-session-id',
244+
bitgoMsg1: await MPSComms.detachSignMpsMessage(Buffer.from(bitgoRawMsg1.payload), bitgoPrvKeyObj),
245+
};
246+
});
247+
}
248+
249+
async function nockMPSKeyGenRound2(
250+
bitgoSession: EddsaMPSDkg.DKG,
251+
bitgoState: { msg2?: MPSTypes.DeserializedMessage },
252+
times = 1
253+
) {
254+
return nock(bgUrl)
255+
.post('/api/v2/mpc/generatekey', (body) => body.round === 'MPS-R2')
256+
.times(times)
257+
.reply(200, async (_uri, { payload }: { payload: EddsaMPCv2KeyGenRound2Request }) => {
258+
const { sessionId, userMsg2, backupMsg2 } = payload;
259+
260+
openpgp.config.rejectCurves = new Set();
261+
262+
assert.ok(bitgoState.msg2, 'BitGo round-2 message missing — round-1 nock must run first');
263+
264+
const userDeserMsg2: MPSTypes.DeserializedMessage = {
265+
from: 0,
266+
payload: new Uint8Array(Buffer.from(userMsg2.message, 'base64')),
267+
};
268+
const backupDeserMsg2: MPSTypes.DeserializedMessage = {
269+
from: 1,
270+
payload: new Uint8Array(Buffer.from(backupMsg2.message, 'base64')),
271+
};
272+
273+
// Complete DKG with all 3 round-2 messages (user, backup, and BitGo's own msg2)
274+
bitgoSession.handleIncomingMessages([userDeserMsg2, backupDeserMsg2, bitgoState.msg2]);
275+
276+
return {
277+
sessionId,
278+
commonPublicKey: bitgoSession.getSharePublicKey().toString('hex'),
279+
bitgoMsg2: await MPSComms.detachSignMpsMessage(Buffer.from(bitgoState.msg2.payload), bitgoPrvKeyObj),
280+
};
281+
});
282+
}
283+
284+
async function nockKeychain(
285+
params: { coin: string; keyChain: Keychain; source: 'user' | 'backup' | 'bitgo' },
286+
times = 1
287+
): Promise<Keychain> {
288+
nock(bgUrl)
289+
.post(`/api/v2/${params.coin}/key`, (body) => body.keyType === 'tss' && body.source === params.source)
290+
.times(times)
291+
.reply(200, params.keyChain);
292+
return params.keyChain;
293+
}
294+
295+
async function nockAddKeyChain(coin: string, times = 1) {
296+
return nock(bgUrl)
297+
.post(`/api/v2/${coin}/key`, (body) => body.keyType === 'tss' && body.isMPCv2)
298+
.times(times)
299+
.reply(200, async (_uri, requestBody: AddKeychainOptions) => {
300+
const key = {
301+
id: requestBody.source,
302+
source: requestBody.source,
303+
type: requestBody.keyType,
304+
commonKeychain: requestBody.commonKeychain,
305+
encryptedPrv: requestBody.encryptedPrv,
306+
};
307+
nock(bgUrl).get(`/api/v2/${coin}/key/${requestBody.source}`).reply(200, key);
308+
return key;
309+
});
310+
}
311+
});
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { IBaseCoin } from '../../../baseCoin';
2+
import baseTSSUtils from '../baseTSSUtils';
3+
import { KeyShare } from './types';
4+
import { BitGoBase } from '../../../bitgoBase';
5+
import { IWallet } from '../../../wallet';
6+
7+
export class BaseEddsaUtils extends baseTSSUtils<KeyShare> {
8+
constructor(bitgo: BitGoBase, baseCoin: IBaseCoin, wallet?: IWallet) {
9+
super(bitgo, baseCoin, wallet);
10+
this.setBitgoGpgPubKey(bitgo);
11+
}
12+
}

0 commit comments

Comments
 (0)