Skip to content

Commit 9f00d3f

Browse files
authored
Merge pull request #8724 from BitGo/WCI-372/eddsa-mpcv2-dsg-post-merge-nits-v2
refactor(sdk-core): eddsa MPCv2 DSG post-merge nits
2 parents 751bd06 + c3dee70 commit 9f00d3f

4 files changed

Lines changed: 119 additions & 38 deletions

File tree

modules/bitgo/test/v2/unit/internal/tssUtils/eddsaMPCv2/signTxRequest.ts

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,90 @@ describe('signTxRequest:', function () {
239239
.should.be.rejectedWith(/Unexpected signature share response/);
240240
});
241241

242+
it('should throw if round 2 response has wrong type', async function () {
243+
const messageBuffer = Buffer.from(signableHex, 'hex');
244+
const bitgoDsg = new EddsaMPSDsg.DSG(MPCv2PartiesEnum.BITGO);
245+
bitgoDsg.initDsg(
246+
bitgoKeyShare,
247+
messageBuffer,
248+
txRequest.transactions![0].unsignedTx.derivationPath,
249+
MPCv2PartiesEnum.USER
250+
);
251+
const bitgoMsg1 = bitgoDsg.getFirstMessage();
252+
253+
// Round 1: return a valid round1Output so the orchestration can proceed
254+
nock('https://bitgo.fakeurl')
255+
.post(
256+
`/api/v2/wallet/${txRequest.walletId}/txrequests/${txRequest.txRequestId}/transactions/0/sign`,
257+
(body) =>
258+
(JSON.parse(body.signatureShares[0].share) as EddsaMPCv2SignatureShareRound1Input).type === 'round1Input'
259+
)
260+
.reply(
261+
200,
262+
async (_uri: string, body: { signatureShares: SignatureShareRecord[]; signerGpgPublicKey: string }) => {
263+
const parsedShare = JSON.parse(body.signatureShares[0].share) as EddsaMPCv2SignatureShareRound1Input;
264+
const userMsg1Bytes = Buffer.from(parsedShare.data.msg1.message, 'base64');
265+
const userDeserializedMsg1: MPSTypes.DeserializedMessage = {
266+
from: MPCv2PartiesEnum.USER,
267+
payload: new Uint8Array(userMsg1Bytes),
268+
};
269+
// Advance bitgo session (we don't need bitgoMsg2 for this test)
270+
bitgoDsg.handleIncomingMessages([bitgoMsg1, userDeserializedMsg1]);
271+
const bitgoSignedMsg1 = await MPSComms.detachSignMpsMessage(Buffer.from(bitgoMsg1.payload), bitgoPrvKeyObj);
272+
const round1Output: EddsaMPCv2SignatureShareRound1Output = {
273+
type: 'round1Output',
274+
data: { msg1: bitgoSignedMsg1 },
275+
};
276+
return {
277+
txRequestId,
278+
transactions: [
279+
{
280+
signatureShares: [
281+
{
282+
from: SignatureShareType.BITGO,
283+
to: SignatureShareType.USER,
284+
share: JSON.stringify(round1Output),
285+
},
286+
],
287+
},
288+
],
289+
};
290+
}
291+
);
292+
293+
// Round 2: return a share with wrong type (round3Output instead of round2Output)
294+
nock('https://bitgo.fakeurl')
295+
.post(
296+
`/api/v2/wallet/${txRequest.walletId}/txrequests/${txRequest.txRequestId}/transactions/0/sign`,
297+
(body) =>
298+
(JSON.parse(body.signatureShares[0].share) as EddsaMPCv2SignatureShareRound2Input).type === 'round2Input'
299+
)
300+
.reply(200, {
301+
txRequestId,
302+
transactions: [
303+
{
304+
signatureShares: [
305+
{
306+
from: SignatureShareType.USER,
307+
to: SignatureShareType.BITGO,
308+
share: 'placeholder',
309+
},
310+
{
311+
from: SignatureShareType.BITGO,
312+
to: SignatureShareType.USER,
313+
share: JSON.stringify({ type: 'round3Output', data: {} }),
314+
},
315+
],
316+
},
317+
],
318+
});
319+
320+
const userPrvBase64 = Buffer.from(userKeyShare).toString('base64');
321+
await tssUtils
322+
.signTxRequest({ txRequest, prv: userPrvBase64, reqId, txParams })
323+
.should.be.rejectedWith(/Unexpected signature share response. Unable to parse data./);
324+
});
325+
242326
it('successfully signs a txRequest after receiving multiple 429 errors in round 2', async function () {
243327
const nockPromises = await getNockPromisesForEddsaSigning(txRequest, RequestType.tx, 3);
244328
await Promise.all(nockPromises);

modules/sdk-core/src/bitgo/tss/eddsa/eddsaMPCv2.ts

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import assert from 'assert';
21
import * as openpgp from 'openpgp';
32
import { MPSComms, MPSTypes } from '@bitgo/sdk-lib-mpc';
43
import {
@@ -11,14 +10,15 @@ import {
1110
import { SignatureShareRecord, SignatureShareType } from '../../utils/tss/baseTypes';
1211
import { MPCv2PartiesEnum } from '../../utils/tss/ecdsa/typesMPCv2';
1312

14-
function partyIdToSignatureShareType(partyId: 0 | 1 | 2): SignatureShareType {
15-
assert(partyId === 0 || partyId === 1 || partyId === 2, 'Invalid partyId for EdDSA MPCv2 signing');
13+
type SignerPartyId = MPCv2PartiesEnum.USER | MPCv2PartiesEnum.BACKUP;
14+
15+
function partyIdToSignatureShareType(partyId: MPCv2PartiesEnum): SignatureShareType {
1616
switch (partyId) {
17-
case 0:
17+
case MPCv2PartiesEnum.USER:
1818
return SignatureShareType.USER;
19-
case 1:
19+
case MPCv2PartiesEnum.BACKUP:
2020
return SignatureShareType.BACKUP;
21-
case 2:
21+
case MPCv2PartiesEnum.BITGO:
2222
return SignatureShareType.BITGO;
2323
}
2424
}
@@ -32,8 +32,8 @@ function partyIdToSignatureShareType(partyId: 0 | 1 | 2): SignatureShareType {
3232
export async function getSignatureShareRoundOne(
3333
userMsg1: MPSTypes.DeserializedMessage,
3434
userGpgPrivKey: openpgp.PrivateKey,
35-
partyId: 0 | 1 = 0,
36-
otherSignerPartyId: 0 | 1 | 2 = 2
35+
partyId: SignerPartyId = MPCv2PartiesEnum.USER,
36+
otherSignerPartyId: MPCv2PartiesEnum = MPCv2PartiesEnum.BITGO
3737
): Promise<SignatureShareRecord> {
3838
const signedMsg1 = await MPSComms.detachSignMpsMessage(Buffer.from(userMsg1.payload), userGpgPrivKey);
3939
const share: EddsaMPCv2SignatureShareRound1Input = {
@@ -51,7 +51,7 @@ export async function getSignatureShareRoundOne(
5151
* Verifies the peer's round-1 PGP signature and returns the raw deserialized
5252
* message ready for `DSG.handleIncomingMessages`.
5353
*/
54-
export async function verifyBitGoMessageRoundOne(
54+
export async function verifyPeerMessageRoundOne(
5555
parsedRound1Output: EddsaMPCv2SignatureShareRound1Output,
5656
peerGpgKey: openpgp.Key,
5757
peerPartyId: MPCv2PartiesEnum = MPCv2PartiesEnum.BITGO
@@ -69,8 +69,8 @@ export async function verifyBitGoMessageRoundOne(
6969
export async function getSignatureShareRoundTwo(
7070
userMsg2: MPSTypes.DeserializedMessage,
7171
userGpgPrivKey: openpgp.PrivateKey,
72-
partyId: 0 | 1 = 0,
73-
otherSignerPartyId: 0 | 1 | 2 = 2
72+
partyId: SignerPartyId = MPCv2PartiesEnum.USER,
73+
otherSignerPartyId: MPCv2PartiesEnum = MPCv2PartiesEnum.BITGO
7474
): Promise<SignatureShareRecord> {
7575
const signedMsg2 = await MPSComms.detachSignMpsMessage(Buffer.from(userMsg2.payload), userGpgPrivKey);
7676
const share: EddsaMPCv2SignatureShareRound2Input = {
@@ -88,7 +88,7 @@ export async function getSignatureShareRoundTwo(
8888
* Verifies the peer's round-2 PGP signature and returns the raw deserialized
8989
* message ready for `DSG.handleIncomingMessages`.
9090
*/
91-
export async function verifyBitGoMessageRoundTwo(
91+
export async function verifyPeerMessageRoundTwo(
9292
parsedRound2Output: EddsaMPCv2SignatureShareRound2Output,
9393
peerGpgKey: openpgp.Key,
9494
peerPartyId: MPCv2PartiesEnum = MPCv2PartiesEnum.BITGO
@@ -110,8 +110,8 @@ export async function verifyBitGoMessageRoundTwo(
110110
export async function getSignatureShareRoundThree(
111111
userMsg3: MPSTypes.DeserializedMessage,
112112
userGpgPrivKey: openpgp.PrivateKey,
113-
partyId: 0 | 1 = 0,
114-
otherSignerPartyId: 0 | 1 | 2 = 2
113+
partyId: SignerPartyId = MPCv2PartiesEnum.USER,
114+
otherSignerPartyId: MPCv2PartiesEnum = MPCv2PartiesEnum.BITGO
115115
): Promise<SignatureShareRecord> {
116116
const signedMsg3 = await MPSComms.detachSignMpsMessage(Buffer.from(userMsg3.payload), userGpgPrivKey);
117117
const share: EddsaMPCv2SignatureShareRound3Input = {

modules/sdk-core/src/bitgo/utils/tss/eddsa/eddsaMPCv2.ts

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ import {
2121
getSignatureShareRoundOne,
2222
getSignatureShareRoundTwo,
2323
getSignatureShareRoundThree,
24-
verifyBitGoMessageRoundOne,
25-
verifyBitGoMessageRoundTwo,
24+
verifyPeerMessageRoundOne,
25+
verifyPeerMessageRoundTwo,
2626
} from '../../../tss/eddsa/eddsaMPCv2';
2727
import { generateGPGKeyPair } from '../../opengpgUtils';
2828
import { MPCv2PartiesEnum } from '../ecdsa/typesMPCv2';
@@ -383,10 +383,7 @@ export class EddsaMPCv2Utils extends BaseEddsaUtils {
383383
const userGpgKey = await generateGPGKeyPair('ed25519');
384384
const userGpgPrvKey = await pgp.readPrivateKey({ armoredKey: userGpgKey.privateKey });
385385
const bitgoGpgPubKey = await this.pickBitgoPubGpgKeyForSigning(true, params.reqId, txRequest.enterpriseId, true);
386-
387-
if (!bitgoGpgPubKey) {
388-
throw new Error('Missing BitGo GPG key for MPCv2');
389-
}
386+
assert(bitgoGpgPubKey, 'Missing BitGo GPG key for MPCv2');
390387

391388
if (requestType === RequestType.tx) {
392389
assert(txRequest.transactions || txRequest.unsignedTxs, 'Unable to find transactions in txRequest');
@@ -452,7 +449,7 @@ export class EddsaMPCv2Utils extends BaseEddsaUtils {
452449
throw new Error('Unexpected signature share response. Unable to parse data.');
453450
}
454451

455-
const bitgoDeserializedMsg1 = await verifyBitGoMessageRoundOne(parsedBitGoToUserSigShareRoundOne, bitgoGpgPubKey);
452+
const bitgoDeserializedMsg1 = await verifyPeerMessageRoundOne(parsedBitGoToUserSigShareRoundOne, bitgoGpgPubKey);
456453

457454
// ── WASM Round 1 ──────────────────────────────────────────────────────────
458455
const [userMsg2] = userDsg.handleIncomingMessages([userMsg1, bitgoDeserializedMsg1]);
@@ -491,7 +488,7 @@ export class EddsaMPCv2Utils extends BaseEddsaUtils {
491488
throw new Error('Unexpected signature share response. Unable to parse data.');
492489
}
493490

494-
const bitgoDeserializedMsg2 = await verifyBitGoMessageRoundTwo(parsedBitGoToUserSigShareRoundTwo, bitgoGpgPubKey);
491+
const bitgoDeserializedMsg2 = await verifyPeerMessageRoundTwo(parsedBitGoToUserSigShareRoundTwo, bitgoGpgPubKey);
495492

496493
// ── WASM Round 2 ──────────────────────────────────────────────────────────
497494
const [userMsg3] = userDsg.handleIncomingMessages([userMsg2, bitgoDeserializedMsg2]);

modules/sdk-core/test/unit/bitgo/utils/tss/eddsa/eddsaMPCv2.ts

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ import {
1313
getSignatureShareRoundOne,
1414
getSignatureShareRoundTwo,
1515
getSignatureShareRoundThree,
16-
verifyBitGoMessageRoundOne,
17-
verifyBitGoMessageRoundTwo,
16+
verifyPeerMessageRoundOne,
17+
verifyPeerMessageRoundTwo,
1818
} from '../../../../../../src/bitgo/tss/eddsa/eddsaMPCv2';
1919
import { decodeWithCodec } from '../../../../../../src/bitgo/utils/codecs';
2020
import { generateGPGKeyPair } from '../../../../../../src/bitgo/utils/opengpgUtils';
@@ -96,7 +96,7 @@ describe('EdDSA MPS DSG helper functions', async () => {
9696
assert.ok(parsed.data.msg1.signature, 'msg1.signature should be set');
9797
});
9898

99-
it('verifyBitGoMessageRoundOne should verify a valid BitGo round-1 message', async () => {
99+
it('verifyPeerMessageRoundOne should verify a valid BitGo round-1 message', async () => {
100100
const messageBuffer = Buffer.from(signableHex, 'hex');
101101
const bitgoDsg = new EddsaMPSDsg.DSG(MPCv2PartiesEnum.BITGO);
102102
bitgoDsg.initDsg(bitgoKeyShare, messageBuffer, derivationPath, MPCv2PartiesEnum.USER);
@@ -108,13 +108,13 @@ describe('EdDSA MPS DSG helper functions', async () => {
108108
data: { msg1: bitgoSignedMsg1 },
109109
};
110110

111-
const result = await verifyBitGoMessageRoundOne(round1Output, bitgoGpgPubKey);
111+
const result = await verifyPeerMessageRoundOne(round1Output, bitgoGpgPubKey);
112112

113113
assert.strictEqual(result.from, MPCv2PartiesEnum.BITGO);
114114
assert.ok(result.payload.length > 0, 'payload should be non-empty');
115115
});
116116

117-
it('verifyBitGoMessageRoundOne should throw on a tampered message', async () => {
117+
it('verifyPeerMessageRoundOne should throw on a tampered message', async () => {
118118
const round1Output: EddsaMPCv2SignatureShareRound1Output = {
119119
type: 'round1Output',
120120
data: {
@@ -125,7 +125,7 @@ describe('EdDSA MPS DSG helper functions', async () => {
125125
},
126126
};
127127

128-
await assert.rejects(verifyBitGoMessageRoundOne(round1Output, bitgoGpgPubKey), 'should throw on invalid signature');
128+
await assert.rejects(verifyPeerMessageRoundOne(round1Output, bitgoGpgPubKey), 'should throw on invalid signature');
129129
});
130130

131131
// ── Round 2 ─────────────────────────────────────────────────────────────────
@@ -141,7 +141,7 @@ describe('EdDSA MPS DSG helper functions', async () => {
141141
const bitgoMsg1 = bitgoDsg.getFirstMessage();
142142

143143
const bitgoSignedMsg1 = await MPSComms.detachSignMpsMessage(Buffer.from(bitgoMsg1.payload), bitgoGpgPrivKey);
144-
const bitgoDeserializedMsg1 = await verifyBitGoMessageRoundOne(
144+
const bitgoDeserializedMsg1 = await verifyPeerMessageRoundOne(
145145
{ type: 'round1Output', data: { msg1: bitgoSignedMsg1 } },
146146
bitgoGpgPubKey
147147
);
@@ -173,7 +173,7 @@ describe('EdDSA MPS DSG helper functions', async () => {
173173
const bitgoMsg1 = bitgoDsg.getFirstMessage();
174174

175175
const bitgoSignedMsg1 = await MPSComms.detachSignMpsMessage(Buffer.from(bitgoMsg1.payload), bitgoGpgPrivKey);
176-
const bitgoDeserializedMsg1 = await verifyBitGoMessageRoundOne(
176+
const bitgoDeserializedMsg1 = await verifyPeerMessageRoundOne(
177177
{ type: 'round1Output', data: { msg1: bitgoSignedMsg1 } },
178178
bitgoGpgPubKey
179179
);
@@ -198,7 +198,7 @@ describe('EdDSA MPS DSG helper functions', async () => {
198198
assert.ok(parsed.data.msg2.signature, 'msg2.signature should be set');
199199
});
200200

201-
it('verifyBitGoMessageRoundTwo should verify a valid BitGo round-2 message', async () => {
201+
it('verifyPeerMessageRoundTwo should verify a valid BitGo round-2 message', async () => {
202202
const messageBuffer = Buffer.from(signableHex, 'hex');
203203
const userDsg = new EddsaMPSDsg.DSG(MPCv2PartiesEnum.USER);
204204
userDsg.initDsg(userKeyShare, messageBuffer, derivationPath, MPCv2PartiesEnum.BITGO);
@@ -216,13 +216,13 @@ describe('EdDSA MPS DSG helper functions', async () => {
216216
data: { msg2: bitgoSignedMsg2 },
217217
};
218218

219-
const result = await verifyBitGoMessageRoundTwo(round2Output, bitgoGpgPubKey);
219+
const result = await verifyPeerMessageRoundTwo(round2Output, bitgoGpgPubKey);
220220

221221
assert.strictEqual(result.from, MPCv2PartiesEnum.BITGO);
222222
assert.ok(result.payload.length > 0, 'payload should be non-empty');
223223
});
224224

225-
it('verifyBitGoMessageRoundTwo should throw on a tampered message', async () => {
225+
it('verifyPeerMessageRoundTwo should throw on a tampered message', async () => {
226226
const round2Output: EddsaMPCv2SignatureShareRound2Output = {
227227
type: 'round2Output',
228228
data: {
@@ -233,7 +233,7 @@ describe('EdDSA MPS DSG helper functions', async () => {
233233
},
234234
};
235235

236-
await assert.rejects(verifyBitGoMessageRoundTwo(round2Output, bitgoGpgPubKey), 'should throw on invalid signature');
236+
await assert.rejects(verifyPeerMessageRoundTwo(round2Output, bitgoGpgPubKey), 'should throw on invalid signature');
237237
});
238238

239239
// ── Round 3 ─────────────────────────────────────────────────────────────────
@@ -250,15 +250,15 @@ describe('EdDSA MPS DSG helper functions', async () => {
250250

251251
// Advance to round 2
252252
const bitgoSignedMsg1 = await MPSComms.detachSignMpsMessage(Buffer.from(bitgoMsg1.payload), bitgoGpgPrivKey);
253-
const bitgoDeserializedMsg1 = await verifyBitGoMessageRoundOne(
253+
const bitgoDeserializedMsg1 = await verifyPeerMessageRoundOne(
254254
{ type: 'round1Output', data: { msg1: bitgoSignedMsg1 } },
255255
bitgoGpgPubKey
256256
);
257257
const [userMsg2] = userDsg.handleIncomingMessages([userMsg1, bitgoDeserializedMsg1]);
258258

259259
const [bitgoMsg2] = bitgoDsg.handleIncomingMessages([bitgoMsg1, userMsg1]);
260260
const bitgoSignedMsg2 = await MPSComms.detachSignMpsMessage(Buffer.from(bitgoMsg2.payload), bitgoGpgPrivKey);
261-
const bitgoDeserializedMsg2 = await verifyBitGoMessageRoundTwo(
261+
const bitgoDeserializedMsg2 = await verifyPeerMessageRoundTwo(
262262
{ type: 'round2Output', data: { msg2: bitgoSignedMsg2 } },
263263
bitgoGpgPubKey
264264
);
@@ -290,15 +290,15 @@ describe('EdDSA MPS DSG helper functions', async () => {
290290
const bitgoMsg1 = bitgoDsg.getFirstMessage();
291291

292292
const bitgoSignedMsg1 = await MPSComms.detachSignMpsMessage(Buffer.from(bitgoMsg1.payload), bitgoGpgPrivKey);
293-
const bitgoDeserializedMsg1 = await verifyBitGoMessageRoundOne(
293+
const bitgoDeserializedMsg1 = await verifyPeerMessageRoundOne(
294294
{ type: 'round1Output', data: { msg1: bitgoSignedMsg1 } },
295295
bitgoGpgPubKey
296296
);
297297
const [backupMsg2] = backupDsg.handleIncomingMessages([backupMsg1, bitgoDeserializedMsg1]);
298298

299299
const [bitgoMsg2] = bitgoDsg.handleIncomingMessages([bitgoMsg1, backupMsg1]);
300300
const bitgoSignedMsg2 = await MPSComms.detachSignMpsMessage(Buffer.from(bitgoMsg2.payload), bitgoGpgPrivKey);
301-
const bitgoDeserializedMsg2 = await verifyBitGoMessageRoundTwo(
301+
const bitgoDeserializedMsg2 = await verifyPeerMessageRoundTwo(
302302
{ type: 'round2Output', data: { msg2: bitgoSignedMsg2 } },
303303
bitgoGpgPubKey
304304
);

0 commit comments

Comments
 (0)