From 0be6a600828aa07951b2f4f1b0df5aeb75ff09d0 Mon Sep 17 00:00:00 2001 From: Mohammad Al Faiyaz Date: Thu, 9 Apr 2026 15:07:48 -0400 Subject: [PATCH] fix(sdk-core): verify signatureR consistency across parties in DKLS round 4 Fixes WAL-377 Co-Authored-By: Claude Sonnet 4.6 TICKET: WAL-377 --- modules/sdk-core/src/account-lib/mpc/util.ts | 9 ++- .../test/unit/account-lib/mpc/util.ts | 59 +++++++++++++++++++ 2 files changed, 66 insertions(+), 2 deletions(-) create mode 100644 modules/sdk-core/test/unit/account-lib/mpc/util.ts diff --git a/modules/sdk-core/src/account-lib/mpc/util.ts b/modules/sdk-core/src/account-lib/mpc/util.ts index 3af40b10c8..acace265f2 100644 --- a/modules/sdk-core/src/account-lib/mpc/util.ts +++ b/modules/sdk-core/src/account-lib/mpc/util.ts @@ -19,10 +19,15 @@ export function combineRound4DklsDsgMessages( round4DsgMessages: DklsTypes.SerializedBroadcastMessage[] ): DklsTypes.SerializedDklsSignature { const round4DsgMessagesDeser = round4DsgMessages.map(DklsTypes.deserializeBroadcastMessage); - const signatureR = round4DsgMessagesDeser.find((m) => m.signatureR !== undefined)?.signatureR; - if (!signatureR) { + const messagesWithR = round4DsgMessagesDeser.filter((m) => m.signatureR !== undefined); + if (messagesWithR.length === 0) { throw Error('None of the round 4 Dkls messages contain a Signature.R value.'); } + const rValues = messagesWithR.map((m) => Buffer.from(m.signatureR as Uint8Array).toString('hex')); + if (!rValues.every((r) => r === rValues[0])) { + throw new Error('signatureR mismatch across parties — possible protocol attack'); + } + const signatureR = messagesWithR[0].signatureR as Uint8Array; const signatureDeser = DklsUtils.combinePartialSignatures( round4DsgMessagesDeser.map((m) => m.payload), Buffer.from(signatureR).toString('hex') diff --git a/modules/sdk-core/test/unit/account-lib/mpc/util.ts b/modules/sdk-core/test/unit/account-lib/mpc/util.ts new file mode 100644 index 0000000000..47cf4547f3 --- /dev/null +++ b/modules/sdk-core/test/unit/account-lib/mpc/util.ts @@ -0,0 +1,59 @@ +import 'should'; +import sinon from 'sinon'; +import { DklsUtils, DklsTypes } from '@bitgo/sdk-lib-mpc'; +import { combineRound4DklsDsgMessages } from '../../../../src/account-lib/mpc/util'; + +function makeMsg(from: number, rHex?: string): DklsTypes.SerializedBroadcastMessage { + return { + payload: Buffer.from(`payload-${from}`).toString('base64'), + from, + signatureR: rHex ? Buffer.from(rHex, 'hex').toString('base64') : undefined, + }; +} + +describe('combineRound4DklsDsgMessages', function () { + let stub: sinon.SinonStub; + + beforeEach(function () { + stub = sinon.stub(DklsUtils, 'combinePartialSignatures').returns({ + R: new Uint8Array([1, 2, 3]), + S: new Uint8Array([4, 5, 6]), + }); + }); + + afterEach(function () { + stub.restore(); + }); + + it('throws when no message contains signatureR', function () { + const msgs = [makeMsg(0), makeMsg(1), makeMsg(2)]; + (() => combineRound4DklsDsgMessages(msgs)).should.throw( + 'None of the round 4 Dkls messages contain a Signature.R value.' + ); + }); + + it('throws when parties provide different signatureR values', function () { + const msgs = [makeMsg(0, 'aabbcc'), makeMsg(1, 'ddeeff'), makeMsg(2, 'aabbcc')]; + (() => combineRound4DklsDsgMessages(msgs)).should.throw( + 'signatureR mismatch across parties — possible protocol attack' + ); + }); + + it('succeeds when all parties agree on signatureR', function () { + const rHex = 'aabbccddeeff0011'; + const msgs = [makeMsg(0, rHex), makeMsg(1, rHex), makeMsg(2, rHex)]; + const result = combineRound4DklsDsgMessages(msgs); + result.should.have.property('R'); + result.should.have.property('S'); + stub.calledOnce.should.be.true(); + stub.firstCall.args[1].should.equal(rHex); + }); + + it('succeeds when only one party provides signatureR', function () { + const rHex = 'cafebabe'; + const msgs = [makeMsg(0, rHex), makeMsg(1), makeMsg(2)]; + const result = combineRound4DklsDsgMessages(msgs); + result.should.have.property('R'); + stub.calledOnce.should.be.true(); + }); +});