diff --git a/modules/bitgo/test/v2/unit/internal/tssUtils/eddsaMPCv2/signTxRequest.ts b/modules/bitgo/test/v2/unit/internal/tssUtils/eddsaMPCv2/signTxRequest.ts index 98ec3a6f0e..3b6bf33dc5 100644 --- a/modules/bitgo/test/v2/unit/internal/tssUtils/eddsaMPCv2/signTxRequest.ts +++ b/modules/bitgo/test/v2/unit/internal/tssUtils/eddsaMPCv2/signTxRequest.ts @@ -239,6 +239,90 @@ describe('signTxRequest:', function () { .should.be.rejectedWith(/Unexpected signature share response/); }); + it('should throw if round 2 response has wrong type', async function () { + const messageBuffer = Buffer.from(signableHex, 'hex'); + const bitgoDsg = new EddsaMPSDsg.DSG(MPCv2PartiesEnum.BITGO); + bitgoDsg.initDsg( + bitgoKeyShare, + messageBuffer, + txRequest.transactions![0].unsignedTx.derivationPath, + MPCv2PartiesEnum.USER + ); + const bitgoMsg1 = bitgoDsg.getFirstMessage(); + + // Round 1: return a valid round1Output so the orchestration can proceed + nock('https://bitgo.fakeurl') + .post( + `/api/v2/wallet/${txRequest.walletId}/txrequests/${txRequest.txRequestId}/transactions/0/sign`, + (body) => + (JSON.parse(body.signatureShares[0].share) as EddsaMPCv2SignatureShareRound1Input).type === 'round1Input' + ) + .reply( + 200, + async (_uri: string, body: { signatureShares: SignatureShareRecord[]; signerGpgPublicKey: string }) => { + const parsedShare = JSON.parse(body.signatureShares[0].share) as EddsaMPCv2SignatureShareRound1Input; + const userMsg1Bytes = Buffer.from(parsedShare.data.msg1.message, 'base64'); + const userDeserializedMsg1: MPSTypes.DeserializedMessage = { + from: MPCv2PartiesEnum.USER, + payload: new Uint8Array(userMsg1Bytes), + }; + // Advance bitgo session (we don't need bitgoMsg2 for this test) + bitgoDsg.handleIncomingMessages([bitgoMsg1, userDeserializedMsg1]); + const bitgoSignedMsg1 = await MPSComms.detachSignMpsMessage(Buffer.from(bitgoMsg1.payload), bitgoPrvKeyObj); + const round1Output: EddsaMPCv2SignatureShareRound1Output = { + type: 'round1Output', + data: { msg1: bitgoSignedMsg1 }, + }; + return { + txRequestId, + transactions: [ + { + signatureShares: [ + { + from: SignatureShareType.BITGO, + to: SignatureShareType.USER, + share: JSON.stringify(round1Output), + }, + ], + }, + ], + }; + } + ); + + // Round 2: return a share with wrong type (round3Output instead of round2Output) + nock('https://bitgo.fakeurl') + .post( + `/api/v2/wallet/${txRequest.walletId}/txrequests/${txRequest.txRequestId}/transactions/0/sign`, + (body) => + (JSON.parse(body.signatureShares[0].share) as EddsaMPCv2SignatureShareRound2Input).type === 'round2Input' + ) + .reply(200, { + txRequestId, + transactions: [ + { + signatureShares: [ + { + from: SignatureShareType.USER, + to: SignatureShareType.BITGO, + share: 'placeholder', + }, + { + from: SignatureShareType.BITGO, + to: SignatureShareType.USER, + share: JSON.stringify({ type: 'round3Output', data: {} }), + }, + ], + }, + ], + }); + + const userPrvBase64 = Buffer.from(userKeyShare).toString('base64'); + await tssUtils + .signTxRequest({ txRequest, prv: userPrvBase64, reqId, txParams }) + .should.be.rejectedWith(/Unexpected signature share response. Unable to parse data./); + }); + it('successfully signs a txRequest after receiving multiple 429 errors in round 2', async function () { const nockPromises = await getNockPromisesForEddsaSigning(txRequest, RequestType.tx, 3); await Promise.all(nockPromises); diff --git a/modules/sdk-core/src/bitgo/tss/eddsa/eddsaMPCv2.ts b/modules/sdk-core/src/bitgo/tss/eddsa/eddsaMPCv2.ts index 4e0a62ba4f..b90b755ba6 100644 --- a/modules/sdk-core/src/bitgo/tss/eddsa/eddsaMPCv2.ts +++ b/modules/sdk-core/src/bitgo/tss/eddsa/eddsaMPCv2.ts @@ -1,4 +1,3 @@ -import assert from 'assert'; import * as openpgp from 'openpgp'; import { MPSComms, MPSTypes } from '@bitgo/sdk-lib-mpc'; import { @@ -11,14 +10,15 @@ import { import { SignatureShareRecord, SignatureShareType } from '../../utils/tss/baseTypes'; import { MPCv2PartiesEnum } from '../../utils/tss/ecdsa/typesMPCv2'; -function partyIdToSignatureShareType(partyId: 0 | 1 | 2): SignatureShareType { - assert(partyId === 0 || partyId === 1 || partyId === 2, 'Invalid partyId for EdDSA MPCv2 signing'); +type SignerPartyId = MPCv2PartiesEnum.USER | MPCv2PartiesEnum.BACKUP; + +function partyIdToSignatureShareType(partyId: MPCv2PartiesEnum): SignatureShareType { switch (partyId) { - case 0: + case MPCv2PartiesEnum.USER: return SignatureShareType.USER; - case 1: + case MPCv2PartiesEnum.BACKUP: return SignatureShareType.BACKUP; - case 2: + case MPCv2PartiesEnum.BITGO: return SignatureShareType.BITGO; } } @@ -32,8 +32,8 @@ function partyIdToSignatureShareType(partyId: 0 | 1 | 2): SignatureShareType { export async function getSignatureShareRoundOne( userMsg1: MPSTypes.DeserializedMessage, userGpgPrivKey: openpgp.PrivateKey, - partyId: 0 | 1 = 0, - otherSignerPartyId: 0 | 1 | 2 = 2 + partyId: SignerPartyId = MPCv2PartiesEnum.USER, + otherSignerPartyId: MPCv2PartiesEnum = MPCv2PartiesEnum.BITGO ): Promise { const signedMsg1 = await MPSComms.detachSignMpsMessage(Buffer.from(userMsg1.payload), userGpgPrivKey); const share: EddsaMPCv2SignatureShareRound1Input = { @@ -51,7 +51,7 @@ export async function getSignatureShareRoundOne( * Verifies the peer's round-1 PGP signature and returns the raw deserialized * message ready for `DSG.handleIncomingMessages`. */ -export async function verifyBitGoMessageRoundOne( +export async function verifyPeerMessageRoundOne( parsedRound1Output: EddsaMPCv2SignatureShareRound1Output, peerGpgKey: openpgp.Key, peerPartyId: MPCv2PartiesEnum = MPCv2PartiesEnum.BITGO @@ -69,8 +69,8 @@ export async function verifyBitGoMessageRoundOne( export async function getSignatureShareRoundTwo( userMsg2: MPSTypes.DeserializedMessage, userGpgPrivKey: openpgp.PrivateKey, - partyId: 0 | 1 = 0, - otherSignerPartyId: 0 | 1 | 2 = 2 + partyId: SignerPartyId = MPCv2PartiesEnum.USER, + otherSignerPartyId: MPCv2PartiesEnum = MPCv2PartiesEnum.BITGO ): Promise { const signedMsg2 = await MPSComms.detachSignMpsMessage(Buffer.from(userMsg2.payload), userGpgPrivKey); const share: EddsaMPCv2SignatureShareRound2Input = { @@ -88,7 +88,7 @@ export async function getSignatureShareRoundTwo( * Verifies the peer's round-2 PGP signature and returns the raw deserialized * message ready for `DSG.handleIncomingMessages`. */ -export async function verifyBitGoMessageRoundTwo( +export async function verifyPeerMessageRoundTwo( parsedRound2Output: EddsaMPCv2SignatureShareRound2Output, peerGpgKey: openpgp.Key, peerPartyId: MPCv2PartiesEnum = MPCv2PartiesEnum.BITGO @@ -110,8 +110,8 @@ export async function verifyBitGoMessageRoundTwo( export async function getSignatureShareRoundThree( userMsg3: MPSTypes.DeserializedMessage, userGpgPrivKey: openpgp.PrivateKey, - partyId: 0 | 1 = 0, - otherSignerPartyId: 0 | 1 | 2 = 2 + partyId: SignerPartyId = MPCv2PartiesEnum.USER, + otherSignerPartyId: MPCv2PartiesEnum = MPCv2PartiesEnum.BITGO ): Promise { const signedMsg3 = await MPSComms.detachSignMpsMessage(Buffer.from(userMsg3.payload), userGpgPrivKey); const share: EddsaMPCv2SignatureShareRound3Input = { diff --git a/modules/sdk-core/src/bitgo/utils/tss/eddsa/eddsaMPCv2.ts b/modules/sdk-core/src/bitgo/utils/tss/eddsa/eddsaMPCv2.ts index 1c8cc543cf..6b25c306c5 100644 --- a/modules/sdk-core/src/bitgo/utils/tss/eddsa/eddsaMPCv2.ts +++ b/modules/sdk-core/src/bitgo/utils/tss/eddsa/eddsaMPCv2.ts @@ -21,8 +21,8 @@ import { getSignatureShareRoundOne, getSignatureShareRoundTwo, getSignatureShareRoundThree, - verifyBitGoMessageRoundOne, - verifyBitGoMessageRoundTwo, + verifyPeerMessageRoundOne, + verifyPeerMessageRoundTwo, } from '../../../tss/eddsa/eddsaMPCv2'; import { generateGPGKeyPair } from '../../opengpgUtils'; import { MPCv2PartiesEnum } from '../ecdsa/typesMPCv2'; @@ -363,10 +363,7 @@ export class EddsaMPCv2Utils extends BaseEddsaUtils { const userGpgKey = await generateGPGKeyPair('ed25519'); const userGpgPrvKey = await pgp.readPrivateKey({ armoredKey: userGpgKey.privateKey }); const bitgoGpgPubKey = await this.pickBitgoPubGpgKeyForSigning(true, params.reqId, txRequest.enterpriseId, true); - - if (!bitgoGpgPubKey) { - throw new Error('Missing BitGo GPG key for MPCv2'); - } + assert(bitgoGpgPubKey, 'Missing BitGo GPG key for MPCv2'); if (requestType === RequestType.tx) { assert(txRequest.transactions || txRequest.unsignedTxs, 'Unable to find transactions in txRequest'); @@ -432,7 +429,7 @@ export class EddsaMPCv2Utils extends BaseEddsaUtils { throw new Error('Unexpected signature share response. Unable to parse data.'); } - const bitgoDeserializedMsg1 = await verifyBitGoMessageRoundOne(parsedBitGoToUserSigShareRoundOne, bitgoGpgPubKey); + const bitgoDeserializedMsg1 = await verifyPeerMessageRoundOne(parsedBitGoToUserSigShareRoundOne, bitgoGpgPubKey); // ── WASM Round 1 ────────────────────────────────────────────────────────── const [userMsg2] = userDsg.handleIncomingMessages([userMsg1, bitgoDeserializedMsg1]); @@ -471,7 +468,7 @@ export class EddsaMPCv2Utils extends BaseEddsaUtils { throw new Error('Unexpected signature share response. Unable to parse data.'); } - const bitgoDeserializedMsg2 = await verifyBitGoMessageRoundTwo(parsedBitGoToUserSigShareRoundTwo, bitgoGpgPubKey); + const bitgoDeserializedMsg2 = await verifyPeerMessageRoundTwo(parsedBitGoToUserSigShareRoundTwo, bitgoGpgPubKey); // ── WASM Round 2 ────────────────────────────────────────────────────────── const [userMsg3] = userDsg.handleIncomingMessages([userMsg2, bitgoDeserializedMsg2]); diff --git a/modules/sdk-core/test/unit/bitgo/utils/tss/eddsa/eddsaMPCv2.ts b/modules/sdk-core/test/unit/bitgo/utils/tss/eddsa/eddsaMPCv2.ts index b228d1be96..6b0c1964cb 100644 --- a/modules/sdk-core/test/unit/bitgo/utils/tss/eddsa/eddsaMPCv2.ts +++ b/modules/sdk-core/test/unit/bitgo/utils/tss/eddsa/eddsaMPCv2.ts @@ -13,8 +13,8 @@ import { getSignatureShareRoundOne, getSignatureShareRoundTwo, getSignatureShareRoundThree, - verifyBitGoMessageRoundOne, - verifyBitGoMessageRoundTwo, + verifyPeerMessageRoundOne, + verifyPeerMessageRoundTwo, } from '../../../../../../src/bitgo/tss/eddsa/eddsaMPCv2'; import { decodeWithCodec } from '../../../../../../src/bitgo/utils/codecs'; import { generateGPGKeyPair } from '../../../../../../src/bitgo/utils/opengpgUtils'; @@ -96,7 +96,7 @@ describe('EdDSA MPS DSG helper functions', async () => { assert.ok(parsed.data.msg1.signature, 'msg1.signature should be set'); }); - it('verifyBitGoMessageRoundOne should verify a valid BitGo round-1 message', async () => { + it('verifyPeerMessageRoundOne should verify a valid BitGo round-1 message', async () => { const messageBuffer = Buffer.from(signableHex, 'hex'); const bitgoDsg = new EddsaMPSDsg.DSG(MPCv2PartiesEnum.BITGO); bitgoDsg.initDsg(bitgoKeyShare, messageBuffer, derivationPath, MPCv2PartiesEnum.USER); @@ -108,13 +108,13 @@ describe('EdDSA MPS DSG helper functions', async () => { data: { msg1: bitgoSignedMsg1 }, }; - const result = await verifyBitGoMessageRoundOne(round1Output, bitgoGpgPubKey); + const result = await verifyPeerMessageRoundOne(round1Output, bitgoGpgPubKey); assert.strictEqual(result.from, MPCv2PartiesEnum.BITGO); assert.ok(result.payload.length > 0, 'payload should be non-empty'); }); - it('verifyBitGoMessageRoundOne should throw on a tampered message', async () => { + it('verifyPeerMessageRoundOne should throw on a tampered message', async () => { const round1Output: EddsaMPCv2SignatureShareRound1Output = { type: 'round1Output', data: { @@ -125,7 +125,7 @@ describe('EdDSA MPS DSG helper functions', async () => { }, }; - await assert.rejects(verifyBitGoMessageRoundOne(round1Output, bitgoGpgPubKey), 'should throw on invalid signature'); + await assert.rejects(verifyPeerMessageRoundOne(round1Output, bitgoGpgPubKey), 'should throw on invalid signature'); }); // ── Round 2 ───────────────────────────────────────────────────────────────── @@ -141,7 +141,7 @@ describe('EdDSA MPS DSG helper functions', async () => { const bitgoMsg1 = bitgoDsg.getFirstMessage(); const bitgoSignedMsg1 = await MPSComms.detachSignMpsMessage(Buffer.from(bitgoMsg1.payload), bitgoGpgPrivKey); - const bitgoDeserializedMsg1 = await verifyBitGoMessageRoundOne( + const bitgoDeserializedMsg1 = await verifyPeerMessageRoundOne( { type: 'round1Output', data: { msg1: bitgoSignedMsg1 } }, bitgoGpgPubKey ); @@ -173,7 +173,7 @@ describe('EdDSA MPS DSG helper functions', async () => { const bitgoMsg1 = bitgoDsg.getFirstMessage(); const bitgoSignedMsg1 = await MPSComms.detachSignMpsMessage(Buffer.from(bitgoMsg1.payload), bitgoGpgPrivKey); - const bitgoDeserializedMsg1 = await verifyBitGoMessageRoundOne( + const bitgoDeserializedMsg1 = await verifyPeerMessageRoundOne( { type: 'round1Output', data: { msg1: bitgoSignedMsg1 } }, bitgoGpgPubKey ); @@ -198,7 +198,7 @@ describe('EdDSA MPS DSG helper functions', async () => { assert.ok(parsed.data.msg2.signature, 'msg2.signature should be set'); }); - it('verifyBitGoMessageRoundTwo should verify a valid BitGo round-2 message', async () => { + it('verifyPeerMessageRoundTwo should verify a valid BitGo round-2 message', async () => { const messageBuffer = Buffer.from(signableHex, 'hex'); const userDsg = new EddsaMPSDsg.DSG(MPCv2PartiesEnum.USER); userDsg.initDsg(userKeyShare, messageBuffer, derivationPath, MPCv2PartiesEnum.BITGO); @@ -216,13 +216,13 @@ describe('EdDSA MPS DSG helper functions', async () => { data: { msg2: bitgoSignedMsg2 }, }; - const result = await verifyBitGoMessageRoundTwo(round2Output, bitgoGpgPubKey); + const result = await verifyPeerMessageRoundTwo(round2Output, bitgoGpgPubKey); assert.strictEqual(result.from, MPCv2PartiesEnum.BITGO); assert.ok(result.payload.length > 0, 'payload should be non-empty'); }); - it('verifyBitGoMessageRoundTwo should throw on a tampered message', async () => { + it('verifyPeerMessageRoundTwo should throw on a tampered message', async () => { const round2Output: EddsaMPCv2SignatureShareRound2Output = { type: 'round2Output', data: { @@ -233,7 +233,7 @@ describe('EdDSA MPS DSG helper functions', async () => { }, }; - await assert.rejects(verifyBitGoMessageRoundTwo(round2Output, bitgoGpgPubKey), 'should throw on invalid signature'); + await assert.rejects(verifyPeerMessageRoundTwo(round2Output, bitgoGpgPubKey), 'should throw on invalid signature'); }); // ── Round 3 ───────────────────────────────────────────────────────────────── @@ -250,7 +250,7 @@ describe('EdDSA MPS DSG helper functions', async () => { // Advance to round 2 const bitgoSignedMsg1 = await MPSComms.detachSignMpsMessage(Buffer.from(bitgoMsg1.payload), bitgoGpgPrivKey); - const bitgoDeserializedMsg1 = await verifyBitGoMessageRoundOne( + const bitgoDeserializedMsg1 = await verifyPeerMessageRoundOne( { type: 'round1Output', data: { msg1: bitgoSignedMsg1 } }, bitgoGpgPubKey ); @@ -258,7 +258,7 @@ describe('EdDSA MPS DSG helper functions', async () => { const [bitgoMsg2] = bitgoDsg.handleIncomingMessages([bitgoMsg1, userMsg1]); const bitgoSignedMsg2 = await MPSComms.detachSignMpsMessage(Buffer.from(bitgoMsg2.payload), bitgoGpgPrivKey); - const bitgoDeserializedMsg2 = await verifyBitGoMessageRoundTwo( + const bitgoDeserializedMsg2 = await verifyPeerMessageRoundTwo( { type: 'round2Output', data: { msg2: bitgoSignedMsg2 } }, bitgoGpgPubKey ); @@ -290,7 +290,7 @@ describe('EdDSA MPS DSG helper functions', async () => { const bitgoMsg1 = bitgoDsg.getFirstMessage(); const bitgoSignedMsg1 = await MPSComms.detachSignMpsMessage(Buffer.from(bitgoMsg1.payload), bitgoGpgPrivKey); - const bitgoDeserializedMsg1 = await verifyBitGoMessageRoundOne( + const bitgoDeserializedMsg1 = await verifyPeerMessageRoundOne( { type: 'round1Output', data: { msg1: bitgoSignedMsg1 } }, bitgoGpgPubKey ); @@ -298,7 +298,7 @@ describe('EdDSA MPS DSG helper functions', async () => { const [bitgoMsg2] = bitgoDsg.handleIncomingMessages([bitgoMsg1, backupMsg1]); const bitgoSignedMsg2 = await MPSComms.detachSignMpsMessage(Buffer.from(bitgoMsg2.payload), bitgoGpgPrivKey); - const bitgoDeserializedMsg2 = await verifyBitGoMessageRoundTwo( + const bitgoDeserializedMsg2 = await verifyPeerMessageRoundTwo( { type: 'round2Output', data: { msg2: bitgoSignedMsg2 } }, bitgoGpgPubKey );