From 89afad613e18a9cc8f49bb30a4b30b232bca5c5a Mon Sep 17 00:00:00 2001 From: "vibhavgopalkrishna145@bitgo.com" Date: Tue, 4 Aug 2026 10:09:30 +0000 Subject: [PATCH 1/3] feat(sdk-core): add verifyPeerMessageRoundThree to EddsaDSGMethods Add verifyPeerMessageRoundThree to modules/sdk-core/src/bitgo/tss/eddsa/eddsaMPCv2.ts, mirroring the call signature of verifyPeerMessageRoundOne and verifyPeerMessageRoundTwo. It decodes and GPG-verifies the MPS DSG round-3 message via MPSComms.verifyMpsMessage. Round 3 previously had no such helper, forcing callers to inline MPSComms.verifyMpsMessage directly instead of using the symmetric round1/round2 helpers already exposed by EddsaDSGMethods. Ticket: WCI-1191 Session-Id: 0716649e-75cb-46dd-8d6a-ae0a701167d6 Task-Id: 3a82317d-853d-4878-97a8-50cf6fa938c5 --- .../src/bitgo/tss/eddsa/eddsaMPCv2.ts | 14 ++++++ .../unit/bitgo/utils/tss/eddsa/eddsaMPCv2.ts | 49 +++++++++++++++++++ 2 files changed, 63 insertions(+) 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..cd9b7b9eea 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,54 @@ 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 userDsg = new EddsaMPSDsg.DSG(MPCv2PartiesEnum.USER); + await userDsg.initDsg(userKeyShare, messageBuffer, derivationPath, MPCv2PartiesEnum.BITGO); + const userMsg1 = userDsg.getFirstMessage(); + + const bitgoDsg = new EddsaMPSDsg.DSG(MPCv2PartiesEnum.BITGO); + await bitgoDsg.initDsg(bitgoKeyShare, messageBuffer, derivationPath, MPCv2PartiesEnum.USER); + const bitgoMsg1 = bitgoDsg.getFirstMessage(); + + const [bitgoMsg2] = bitgoDsg.handleIncomingMessages([bitgoMsg1, userMsg1]); + const bitgoSignedMsg1 = await MPSComms.detachSignMpsMessage(Buffer.from(bitgoMsg1.payload), bitgoGpgPrivKey); + const bitgoDeserializedMsg1 = await verifyPeerMessageRoundOne( + { type: 'round1Output', data: { msg1: bitgoSignedMsg1 } }, + bitgoGpgPubKey + ); + const [userMsg2] = userDsg.handleIncomingMessages([userMsg1, bitgoDeserializedMsg1]); + const [bitgoMsg3] = bitgoDsg.handleIncomingMessages([bitgoMsg2, userMsg2]); + 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', () => { From b211da131344044b832c0fd98cceeb96809522a4 Mon Sep 17 00:00:00 2001 From: "vibhavgopalkrishna145@bitgo.com" Date: Tue, 4 Aug 2026 11:12:23 +0000 Subject: [PATCH 2/3] refactor(sdk-core): use executeTillRound in round-3 verify test Replace the manual initDsg/getFirstMessage/handleIncomingMessages handshake in the verifyPeerMessageRoundThree happy-path test with MPSUtil.executeTillRound(2, ...) from @bitgo/sdk-lib-mpc, which already drives two DSG parties to the requested round and is used for the same purpose in sdk-lib-mpc's own DSG tests. This removes duplicated round1/round2 handshake code from the test so it exercises only what verifyPeerMessageRoundThree needs, matching the existing helper pattern instead of re-deriving it inline. Ticket: WCI-1191 Session-Id: 0716649e-75cb-46dd-8d6a-ae0a701167d6 Task-Id: 3a82317d-853d-4878-97a8-50cf6fa938c5 --- .../unit/bitgo/utils/tss/eddsa/eddsaMPCv2.ts | 25 +++++++------------ 1 file changed, 9 insertions(+), 16 deletions(-) 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 cd9b7b9eea..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 @@ -348,22 +348,15 @@ describe('EdDSA MPS DSG helper functions', async () => { it('verifyPeerMessageRoundThree should verify a valid BitGo round-3 message', async () => { const messageBuffer = Buffer.from(signableHex, 'hex'); - const userDsg = new EddsaMPSDsg.DSG(MPCv2PartiesEnum.USER); - await userDsg.initDsg(userKeyShare, messageBuffer, derivationPath, MPCv2PartiesEnum.BITGO); - const userMsg1 = userDsg.getFirstMessage(); - - const bitgoDsg = new EddsaMPSDsg.DSG(MPCv2PartiesEnum.BITGO); - await bitgoDsg.initDsg(bitgoKeyShare, messageBuffer, derivationPath, MPCv2PartiesEnum.USER); - const bitgoMsg1 = bitgoDsg.getFirstMessage(); - - const [bitgoMsg2] = bitgoDsg.handleIncomingMessages([bitgoMsg1, userMsg1]); - const bitgoSignedMsg1 = await MPSComms.detachSignMpsMessage(Buffer.from(bitgoMsg1.payload), bitgoGpgPrivKey); - const bitgoDeserializedMsg1 = await verifyPeerMessageRoundOne( - { type: 'round1Output', data: { msg1: bitgoSignedMsg1 } }, - bitgoGpgPubKey - ); - const [userMsg2] = userDsg.handleIncomingMessages([userMsg1, bitgoDeserializedMsg1]); - const [bitgoMsg3] = bitgoDsg.handleIncomingMessages([bitgoMsg2, userMsg2]); + 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 = { From a930010ab442d5df94f60d1a0cdf623f538d4d3c Mon Sep 17 00:00:00 2001 From: "vibhavgopalkrishna145@bitgo.com" Date: Thu, 6 Aug 2026 06:46:31 +0000 Subject: [PATCH 3/3] feat(sdk-core): bump public-types to use real Round3Output type Bump @bitgo/public-types from 6.48.0 to 6.53.0 in modules/sdk-core, which publishes EddsaMPCv2SignatureShareRound3Output (added in BitGo/public-types#365, ticket WCI-327). Replace the inline structural type `{ data: { msg3: EddsaMPCv2SignedMessage } }` used as a placeholder in verifyPeerMessageRoundThree's parameter with the real codec type, matching how verifyPeerMessageRoundOne/RoundTwo use their respective round-output types. The ticket originally called for the inline type because public-types had no round-3 output type yet; now that it is published, using the real type keeps verifyPeerMessageRoundThree's signature consistent with its round1/round2 siblings instead of carrying a stand-in shape indefinitely. Ticket: WCI-1191 Session-Id: 0716649e-75cb-46dd-8d6a-ae0a701167d6 Task-Id: 3a82317d-853d-4878-97a8-50cf6fa938c5 --- modules/sdk-core/package.json | 2 +- modules/sdk-core/src/bitgo/tss/eddsa/eddsaMPCv2.ts | 4 ++-- .../test/unit/bitgo/utils/tss/eddsa/eddsaMPCv2.ts | 9 +++++---- yarn.lock | 11 +++++++++++ 4 files changed, 19 insertions(+), 7 deletions(-) diff --git a/modules/sdk-core/package.json b/modules/sdk-core/package.json index 9587d02e93..7ad2b5f739 100644 --- a/modules/sdk-core/package.json +++ b/modules/sdk-core/package.json @@ -40,7 +40,7 @@ ] }, "dependencies": { - "@bitgo/public-types": "6.48.0", + "@bitgo/public-types": "6.53.0", "@bitgo/sdk-lib-mpc": "^10.15.0", "@bitgo/secp256k1": "^1.11.0", "@bitgo/sjcl": "^1.1.0", diff --git a/modules/sdk-core/src/bitgo/tss/eddsa/eddsaMPCv2.ts b/modules/sdk-core/src/bitgo/tss/eddsa/eddsaMPCv2.ts index fc9ca74578..47c22b7643 100644 --- a/modules/sdk-core/src/bitgo/tss/eddsa/eddsaMPCv2.ts +++ b/modules/sdk-core/src/bitgo/tss/eddsa/eddsaMPCv2.ts @@ -6,7 +6,7 @@ import { EddsaMPCv2SignatureShareRound2Input, EddsaMPCv2SignatureShareRound2Output, EddsaMPCv2SignatureShareRound3Input, - EddsaMPCv2SignedMessage, + EddsaMPCv2SignatureShareRound3Output, } from '@bitgo/public-types'; import { SignatureShareRecord, SignatureShareType } from '../../utils/tss/baseTypes'; import { MPCv2PartiesEnum } from '../../utils/tss/ecdsa/typesMPCv2'; @@ -106,7 +106,7 @@ export async function verifyPeerMessageRoundTwo( * message ready for `DSG.handleIncomingMessages`. */ export async function verifyPeerMessageRoundThree( - parsedRound3Output: { data: { msg3: EddsaMPCv2SignedMessage } }, + parsedRound3Output: EddsaMPCv2SignatureShareRound3Output, peerGpgKey: openpgp.Key, peerPartyId: MPCv2PartiesEnum = MPCv2PartiesEnum.BITGO ): Promise { 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 099ee07886..dbc8572d92 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 @@ -11,6 +11,7 @@ import { EddsaMPCv2SignatureShareRound2Input, EddsaMPCv2SignatureShareRound2Output, EddsaMPCv2SignatureShareRound3Input, + EddsaMPCv2SignatureShareRound3Output, } from '@bitgo/public-types'; import { BitGoBase, @@ -359,8 +360,8 @@ describe('EdDSA MPS DSG helper functions', async () => { )) as MPSTypes.DeserializedMessages[]; const bitgoSignedMsg3 = await MPSComms.detachSignMpsMessage(Buffer.from(bitgoMsg3.payload), bitgoGpgPrivKey); - const round3Output = { - type: 'round3Output' as const, + const round3Output: EddsaMPCv2SignatureShareRound3Output = { + type: 'round3Output', data: { msg3: bitgoSignedMsg3 }, }; @@ -371,8 +372,8 @@ describe('EdDSA MPS DSG helper functions', async () => { }); it('verifyPeerMessageRoundThree should throw on a tampered message', async () => { - const round3Output = { - type: 'round3Output' as const, + const round3Output: EddsaMPCv2SignatureShareRound3Output = { + type: 'round3Output', data: { msg3: { message: Buffer.from('tampered').toString('base64'), diff --git a/yarn.lock b/yarn.lock index d6b8955a57..742bde7e38 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1043,6 +1043,17 @@ monocle-ts "^2.3.13" newtype-ts "^0.3.5" +"@bitgo/public-types@6.53.0": + version "6.53.0" + resolved "https://registry.npmjs.org/@bitgo/public-types/-/public-types-6.53.0.tgz#fedaf8dbdc7f5fbe25279e560fd76ed25d9443ed" + integrity sha512-nei+2f2fmrnqVZnQ2GBVSbHy0O5bZwcfp/R1jKpuYFOWQgeZtFmblVMhnbN1sfFVyAAGOdacZtx7d56+qZ0TNA== + dependencies: + fp-ts "^2.0.0" + io-ts "npm:@bitgo-forks/io-ts@2.1.4" + io-ts-types "^0.5.16" + monocle-ts "^2.3.13" + newtype-ts "^0.3.5" + "@bitgo/wasm-dot@^1.7.0": version "1.7.0" resolved "https://registry.npmjs.org/@bitgo/wasm-dot/-/wasm-dot-1.7.0.tgz"