Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
28 changes: 14 additions & 14 deletions modules/sdk-core/src/bitgo/tss/eddsa/eddsaMPCv2.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import assert from 'assert';
import * as openpgp from 'openpgp';
import { MPSComms, MPSTypes } from '@bitgo/sdk-lib-mpc';
import {
Expand All @@ -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;
}
}
Expand All @@ -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<SignatureShareRecord> {
const signedMsg1 = await MPSComms.detachSignMpsMessage(Buffer.from(userMsg1.payload), userGpgPrivKey);
const share: EddsaMPCv2SignatureShareRound1Input = {
Expand All @@ -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
Expand All @@ -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<SignatureShareRecord> {
const signedMsg2 = await MPSComms.detachSignMpsMessage(Buffer.from(userMsg2.payload), userGpgPrivKey);
const share: EddsaMPCv2SignatureShareRound2Input = {
Expand All @@ -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
Expand All @@ -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<SignatureShareRecord> {
const signedMsg3 = await MPSComms.detachSignMpsMessage(Buffer.from(userMsg3.payload), userGpgPrivKey);
const share: EddsaMPCv2SignatureShareRound3Input = {
Expand Down
13 changes: 5 additions & 8 deletions modules/sdk-core/src/bitgo/utils/tss/eddsa/eddsaMPCv2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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');
Expand Down Expand Up @@ -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]);
Expand Down Expand Up @@ -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]);
Expand Down
32 changes: 16 additions & 16 deletions modules/sdk-core/test/unit/bitgo/utils/tss/eddsa/eddsaMPCv2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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);
Expand All @@ -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: {
Expand All @@ -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 ─────────────────────────────────────────────────────────────────
Expand All @@ -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
);
Expand Down Expand Up @@ -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
);
Expand All @@ -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);
Expand All @@ -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: {
Expand All @@ -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 ─────────────────────────────────────────────────────────────────
Expand All @@ -250,15 +250,15 @@ 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
);
const [userMsg2] = userDsg.handleIncomingMessages([userMsg1, bitgoDeserializedMsg1]);

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
);
Expand Down Expand Up @@ -290,15 +290,15 @@ 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
);
const [backupMsg2] = backupDsg.handleIncomingMessages([backupMsg1, bitgoDeserializedMsg1]);

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
);
Expand Down
Loading