diff --git a/modules/sdk-core/src/bitgo/tss/eddsa/eddsaMPCv2.ts b/modules/sdk-core/src/bitgo/tss/eddsa/eddsaMPCv2.ts index b90b755ba6..fc9ca74578 100644 --- a/modules/sdk-core/src/bitgo/tss/eddsa/eddsaMPCv2.ts +++ b/modules/sdk-core/src/bitgo/tss/eddsa/eddsaMPCv2.ts @@ -6,6 +6,7 @@ import { EddsaMPCv2SignatureShareRound2Input, EddsaMPCv2SignatureShareRound2Output, EddsaMPCv2SignatureShareRound3Input, + EddsaMPCv2SignedMessage, } from '@bitgo/public-types'; import { SignatureShareRecord, SignatureShareType } from '../../utils/tss/baseTypes'; import { MPCv2PartiesEnum } from '../../utils/tss/ecdsa/typesMPCv2'; @@ -100,6 +101,19 @@ export async function verifyPeerMessageRoundTwo( }; } +/** + * Verifies the peer's round-3 PGP signature and returns the raw deserialized + * message ready for `DSG.handleIncomingMessages`. + */ +export async function verifyPeerMessageRoundThree( + parsedRound3Output: { data: { msg3: EddsaMPCv2SignedMessage } }, + peerGpgKey: openpgp.Key, + peerPartyId: MPCv2PartiesEnum = MPCv2PartiesEnum.BITGO +): Promise { + const rawBytes = await MPSComms.verifyMpsMessage(parsedRound3Output.data.msg3, peerGpgKey); + return { from: peerPartyId, payload: new Uint8Array(rawBytes) }; +} + /** * Builds the round-3 signature share record (final signer message). * 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 664eab8e09..099ee07886 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 @@ -34,6 +34,7 @@ import { getSignatureShareRoundThree, verifyPeerMessageRoundOne, verifyPeerMessageRoundTwo, + verifyPeerMessageRoundThree, } from '../../../../../../src/bitgo/tss/eddsa/eddsaMPCv2'; import { getInitializedMpcInstance } from '../../../../../../src/bitgo/tss/eddsa/eddsa'; import { getBitgoSignatureShare } from '../../../../../../src/bitgo/tss/common'; @@ -344,6 +345,47 @@ describe('EdDSA MPS DSG helper functions', async () => { assert.ok(parsed.data.msg3.message, 'msg3.message should be set'); assert.ok(parsed.data.msg3.signature, 'msg3.signature should be set'); }); + + it('verifyPeerMessageRoundThree should verify a valid BitGo round-3 message', async () => { + const messageBuffer = Buffer.from(signableHex, 'hex'); + const [, [bitgoMsg3]] = (await MPSUtil.executeTillRound( + 2, + new EddsaMPSDsg.DSG(MPCv2PartiesEnum.USER), + new EddsaMPSDsg.DSG(MPCv2PartiesEnum.BITGO), + userKeyShare, + bitgoKeyShare, + messageBuffer, + derivationPath + )) as MPSTypes.DeserializedMessages[]; + const bitgoSignedMsg3 = await MPSComms.detachSignMpsMessage(Buffer.from(bitgoMsg3.payload), bitgoGpgPrivKey); + + const round3Output = { + type: 'round3Output' as const, + data: { msg3: bitgoSignedMsg3 }, + }; + + const result = await verifyPeerMessageRoundThree(round3Output, bitgoGpgPubKey); + + assert.strictEqual(result.from, MPCv2PartiesEnum.BITGO); + assert.ok(result.payload.length > 0, 'payload should be non-empty'); + }); + + it('verifyPeerMessageRoundThree should throw on a tampered message', async () => { + const round3Output = { + type: 'round3Output' as const, + data: { + msg3: { + message: Buffer.from('tampered').toString('base64'), + signature: '-----BEGIN PGP SIGNATURE-----\n\nINVALID\n-----END PGP SIGNATURE-----\n', + }, + }, + }; + + await assert.rejects( + verifyPeerMessageRoundThree(round3Output, bitgoGpgPubKey), + 'should throw on invalid signature' + ); + }); }); describe('getEddsaMPCv2RecoveryKeyShares', () => {