Skip to content
Open
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
2 changes: 2 additions & 0 deletions modules/sdk-core/src/bitgo/tss/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,8 @@ export async function sendSignatureShareV2(
let type = '';
if (multisigTypeVersion === 'MPCv2' && mpcAlgorithm === 'ecdsa') {
type = 'ecdsaMpcV2';
} else if (multisigTypeVersion === 'MPCv2' && mpcAlgorithm === 'eddsa') {
type = 'eddsaMpcV2';
} else if (multisigTypeVersion === undefined && mpcAlgorithm === 'eddsa') {
type = 'eddsaMpcV1';
}
Expand Down
126 changes: 126 additions & 0 deletions modules/sdk-core/src/bitgo/tss/eddsa/eddsaMPCv2.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import assert from 'assert';
import * as openpgp from 'openpgp';
import { MPSComms, MPSTypes } from '@bitgo/sdk-lib-mpc';
import {
EddsaMPCv2SignatureShareRound1Input,
EddsaMPCv2SignatureShareRound1Output,
EddsaMPCv2SignatureShareRound2Input,
EddsaMPCv2SignatureShareRound2Output,
EddsaMPCv2SignatureShareRound3Input,
} from '@bitgo/public-types';
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');
switch (partyId) {
case 0:
return SignatureShareType.USER;
case 1:
return SignatureShareType.BACKUP;
case 2:
return SignatureShareType.BITGO;
}
}

/**
* Builds the round-1 signature share record.
*
* PGP-signs the WASM round-0 broadcast message with the signer's ephemeral key and
* wraps it into a SignatureShareRecord ready for `sendSignatureShareV2`.
*/
export async function getEddsaSignatureShareRound1(
userMsg1: MPSTypes.DeserializedMessage,
userGpgPrivKey: openpgp.PrivateKey,
partyId: 0 | 1 = 0,
otherSignerPartyId: 0 | 1 | 2 = 2
): Promise<SignatureShareRecord> {
const signedMsg1 = await MPSComms.detachSignMpsMessage(Buffer.from(userMsg1.payload), userGpgPrivKey);
const share: EddsaMPCv2SignatureShareRound1Input = {
type: 'round1Input',
data: { msg1: signedMsg1 },
};
return {
from: partyIdToSignatureShareType(partyId),
to: partyIdToSignatureShareType(otherSignerPartyId),
share: JSON.stringify(share),
};
}

/**
* Verifies the peer's round-1 PGP signature and returns the raw deserialized
* message ready for `DSG.handleIncomingMessages`.
*/
export async function verifyBitGoEddsaMessageRound1(
parsedRound1Output: EddsaMPCv2SignatureShareRound1Output,
bitgoGpgKey: openpgp.Key,
peerPartyId: 0 | 1 | 2 = 2
): Promise<MPSTypes.DeserializedMessage> {
const rawBytes = await MPSComms.verifyMpsMessage(parsedRound1Output.data.msg1, bitgoGpgKey);
return {
from: peerPartyId as MPCv2PartiesEnum,
payload: new Uint8Array(rawBytes),
};
}

/**
* Builds the round-2 signature share record.
*/
export async function getEddsaSignatureShareRound2(
userMsg2: MPSTypes.DeserializedMessage,
userGpgPrivKey: openpgp.PrivateKey,
partyId: 0 | 1 = 0,
otherSignerPartyId: 0 | 1 | 2 = 2
): Promise<SignatureShareRecord> {
const signedMsg2 = await MPSComms.detachSignMpsMessage(Buffer.from(userMsg2.payload), userGpgPrivKey);
const share: EddsaMPCv2SignatureShareRound2Input = {
type: 'round2Input',
data: { msg2: signedMsg2 },
};
return {
from: partyIdToSignatureShareType(partyId),
to: partyIdToSignatureShareType(otherSignerPartyId),
share: JSON.stringify(share),
};
}

/**
* Verifies the peer's round-2 PGP signature and returns the raw deserialized
* message ready for `DSG.handleIncomingMessages`.
*/
export async function verifyBitGoEddsaMessageRound2(
parsedRound2Output: EddsaMPCv2SignatureShareRound2Output,
bitgoGpgKey: openpgp.Key,
peerPartyId: 0 | 1 | 2 = 2
): Promise<MPSTypes.DeserializedMessage> {
const rawBytes = await MPSComms.verifyMpsMessage(parsedRound2Output.data.msg2, bitgoGpgKey);
return {
from: peerPartyId as MPCv2PartiesEnum,
payload: new Uint8Array(rawBytes),
};
}

/**
* Builds the round-3 signature share record (final signer message).
*
* There is no corresponding `verifyBitGoEddsaMessageRound3` because Wallet Platform
* finalises the signing server-side after receiving round 3; the client obtains the
* signed transaction via `sendTxRequest`.
*/
export async function getEddsaSignatureShareRound3(
userMsg3: MPSTypes.DeserializedMessage,
userGpgPrivKey: openpgp.PrivateKey,
partyId: 0 | 1 = 0,
otherSignerPartyId: 0 | 1 | 2 = 2
): Promise<SignatureShareRecord> {
const signedMsg3 = await MPSComms.detachSignMpsMessage(Buffer.from(userMsg3.payload), userGpgPrivKey);
const share: EddsaMPCv2SignatureShareRound3Input = {
type: 'round3Input',
data: { msg3: signedMsg3 },
};
return {
from: partyIdToSignatureShareType(partyId),
to: partyIdToSignatureShareType(otherSignerPartyId),
share: JSON.stringify(share),
};
}
2 changes: 2 additions & 0 deletions modules/sdk-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import { EcdsaUtils } from './bitgo/utils/tss/ecdsa/ecdsa';
export { EcdsaUtils };
import { EcdsaMPCv2Utils } from './bitgo/utils/tss/ecdsa/ecdsaMPCv2';
export { EcdsaMPCv2Utils };
import { EddsaMPCv2Utils } from './bitgo/utils/tss/eddsa/eddsaMPCv2';
export { EddsaMPCv2Utils };
export { verifyEddsaTssWalletAddress, verifyMPCWalletAddress } from './bitgo/utils/tss/addressVerification';
export { GShare, SignShare, YShare } from './account-lib/mpc/tss/eddsa/types';
export { TssEcdsaStep1ReturnMessage, TssEcdsaStep2ReturnMessage } from './bitgo/tss/types';
Expand Down
206 changes: 206 additions & 0 deletions modules/sdk-core/test/unit/bitgo/utils/tss/eddsa/eddsaMPCv2.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
import * as assert from 'assert';
import * as pgp from 'openpgp';
import { EddsaMPSDsg, MPSComms, MPSUtil } from '@bitgo/sdk-lib-mpc';
import {
EddsaMPCv2SignatureShareRound1Input,
EddsaMPCv2SignatureShareRound1Output,
EddsaMPCv2SignatureShareRound2Input,
EddsaMPCv2SignatureShareRound2Output,
EddsaMPCv2SignatureShareRound3Input,
} from '@bitgo/public-types';
import { SignatureShareRecord, SignatureShareType } from '../../../../../../src';
import {
getEddsaSignatureShareRound1,
getEddsaSignatureShareRound2,
getEddsaSignatureShareRound3,
verifyBitGoEddsaMessageRound1,
verifyBitGoEddsaMessageRound2,
} from '../../../../../../src/bitgo/tss/eddsa/eddsaMPCv2';
import { decodeWithCodec } from '../../../../../../src/bitgo/utils/codecs';
import { generateGPGKeyPair } from '../../../../../../src/bitgo/utils/opengpgUtils';
import { MPCv2PartiesEnum } from '../../../../../../src/bitgo/utils/tss/ecdsa/typesMPCv2';

describe('EdDSA MPS DSG helper functions', async () => {
let userKeyShare: Buffer;
let bitgoKeyShare: Buffer;
let userGpgPrivKey: pgp.PrivateKey;
let bitgoGpgPrivKey: pgp.PrivateKey;
let bitgoGpgPubKey: pgp.Key;

const signableHex = 'deadbeef';
const derivationPath = 'm/0';

before('generate EdDSA DKG key shares', async () => {
const userGpgKeyPair = await generateGPGKeyPair('ed25519');
const bitgoGpgKeyPair = await generateGPGKeyPair('ed25519');

userGpgPrivKey = await pgp.readPrivateKey({ armoredKey: userGpgKeyPair.privateKey });
bitgoGpgPrivKey = await pgp.readPrivateKey({ armoredKey: bitgoGpgKeyPair.privateKey });
bitgoGpgPubKey = await pgp.readKey({ armoredKey: bitgoGpgKeyPair.publicKey });

const [userDkg, , bitgoDkg] = await MPSUtil.generateEdDsaDKGKeyShares();
userKeyShare = userDkg.getKeyShare();
bitgoKeyShare = bitgoDkg.getKeyShare();
});

// ── Round 1 ─────────────────────────────────────────────────────────────────

it('getEddsaSignatureShareRound1 should build a valid round-1 share', async () => {
const messageBuffer = Buffer.from(signableHex, 'hex');
const userDsg = new EddsaMPSDsg.DSG(MPCv2PartiesEnum.USER);
userDsg.initDsg(userKeyShare, messageBuffer, derivationPath, MPCv2PartiesEnum.BITGO);
const userMsg1 = userDsg.getFirstMessage();

const share: SignatureShareRecord = await getEddsaSignatureShareRound1(userMsg1, userGpgPrivKey);

assert.strictEqual(share.from, SignatureShareType.USER);
assert.strictEqual(share.to, SignatureShareType.BITGO);

const parsed = decodeWithCodec(
EddsaMPCv2SignatureShareRound1Input,
JSON.parse(share.share),
'EddsaMPCv2SignatureShareRound1Input'
);
assert.strictEqual(parsed.type, 'round1Input');
assert.ok(parsed.data.msg1.message, 'msg1.message should be set');
assert.ok(parsed.data.msg1.signature, 'msg1.signature should be set');
});

it('verifyBitGoEddsaMessageRound1 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);
const bitgoMsg1 = bitgoDsg.getFirstMessage();

const bitgoSignedMsg1 = await MPSComms.detachSignMpsMessage(Buffer.from(bitgoMsg1.payload), bitgoGpgPrivKey);
const round1Output: EddsaMPCv2SignatureShareRound1Output = {
type: 'round1Output',
data: { msg1: bitgoSignedMsg1 },
};

const result = await verifyBitGoEddsaMessageRound1(round1Output, bitgoGpgPubKey);

assert.strictEqual(result.from, MPCv2PartiesEnum.BITGO);
assert.ok(result.payload.length > 0, 'payload should be non-empty');
});

it('verifyBitGoEddsaMessageRound1 should throw on a tampered message', async () => {
const round1Output: EddsaMPCv2SignatureShareRound1Output = {
type: 'round1Output',
data: {
msg1: {
message: Buffer.from('tampered').toString('base64'),
signature: '-----BEGIN PGP SIGNATURE-----\n\nINVALID\n-----END PGP SIGNATURE-----\n',
},
},
};

await assert.rejects(
verifyBitGoEddsaMessageRound1(round1Output, bitgoGpgPubKey),
'should throw on invalid signature'
);
});

// ── Round 2 ─────────────────────────────────────────────────────────────────

it('getEddsaSignatureShareRound2 should build a valid round-2 share', async () => {
const messageBuffer = Buffer.from(signableHex, 'hex');
const userDsg = new EddsaMPSDsg.DSG(MPCv2PartiesEnum.USER);
userDsg.initDsg(userKeyShare, messageBuffer, derivationPath, MPCv2PartiesEnum.BITGO);
const userMsg1 = userDsg.getFirstMessage();

const bitgoDsg = new EddsaMPSDsg.DSG(MPCv2PartiesEnum.BITGO);
bitgoDsg.initDsg(bitgoKeyShare, messageBuffer, derivationPath, MPCv2PartiesEnum.USER);
const bitgoMsg1 = bitgoDsg.getFirstMessage();

const bitgoSignedMsg1 = await MPSComms.detachSignMpsMessage(Buffer.from(bitgoMsg1.payload), bitgoGpgPrivKey);
const bitgoDeserializedMsg1 = await verifyBitGoEddsaMessageRound1(
{ type: 'round1Output', data: { msg1: bitgoSignedMsg1 } },
bitgoGpgPubKey
);
const [userMsg2] = userDsg.handleIncomingMessages([userMsg1, bitgoDeserializedMsg1]);

const share: SignatureShareRecord = await getEddsaSignatureShareRound2(userMsg2, userGpgPrivKey);

assert.strictEqual(share.from, SignatureShareType.USER);
assert.strictEqual(share.to, SignatureShareType.BITGO);

const parsed = decodeWithCodec(
EddsaMPCv2SignatureShareRound2Input,
JSON.parse(share.share),
'EddsaMPCv2SignatureShareRound2Input'
);
assert.strictEqual(parsed.type, 'round2Input');
assert.ok(parsed.data.msg2.message, 'msg2.message should be set');
assert.ok(parsed.data.msg2.signature, 'msg2.signature should be set');
});

it('verifyBitGoEddsaMessageRound2 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);
const userMsg1 = userDsg.getFirstMessage();

const bitgoDsg = new EddsaMPSDsg.DSG(MPCv2PartiesEnum.BITGO);
bitgoDsg.initDsg(bitgoKeyShare, messageBuffer, derivationPath, MPCv2PartiesEnum.USER);
const bitgoMsg1 = bitgoDsg.getFirstMessage();

const bitgoSignedMsg1 = await MPSComms.detachSignMpsMessage(Buffer.from(bitgoMsg1.payload), bitgoGpgPrivKey);
const [bitgoMsg2] = bitgoDsg.handleIncomingMessages([bitgoMsg1, userMsg1]);
const bitgoSignedMsg2 = await MPSComms.detachSignMpsMessage(Buffer.from(bitgoMsg2.payload), bitgoGpgPrivKey);

const round2Output: EddsaMPCv2SignatureShareRound2Output = {
type: 'round2Output',
data: { msg2: bitgoSignedMsg2 },
};

void bitgoSignedMsg1;
const result = await verifyBitGoEddsaMessageRound2(round2Output, bitgoGpgPubKey);

assert.strictEqual(result.from, MPCv2PartiesEnum.BITGO);
assert.ok(result.payload.length > 0, 'payload should be non-empty');
});

// ── Round 3 ─────────────────────────────────────────────────────────────────

it('getEddsaSignatureShareRound3 should build a valid round-3 share', async () => {
const messageBuffer = Buffer.from(signableHex, 'hex');
const userDsg = new EddsaMPSDsg.DSG(MPCv2PartiesEnum.USER);
userDsg.initDsg(userKeyShare, messageBuffer, derivationPath, MPCv2PartiesEnum.BITGO);
const userMsg1 = userDsg.getFirstMessage();

const bitgoDsg = new EddsaMPSDsg.DSG(MPCv2PartiesEnum.BITGO);
bitgoDsg.initDsg(bitgoKeyShare, messageBuffer, derivationPath, MPCv2PartiesEnum.USER);
const bitgoMsg1 = bitgoDsg.getFirstMessage();

// Advance to round 2
const bitgoSignedMsg1 = await MPSComms.detachSignMpsMessage(Buffer.from(bitgoMsg1.payload), bitgoGpgPrivKey);
const bitgoDeserializedMsg1 = await verifyBitGoEddsaMessageRound1(
{ 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 verifyBitGoEddsaMessageRound2(
{ type: 'round2Output', data: { msg2: bitgoSignedMsg2 } },
bitgoGpgPubKey
);
const [userMsg3] = userDsg.handleIncomingMessages([userMsg2, bitgoDeserializedMsg2]);

const share: SignatureShareRecord = await getEddsaSignatureShareRound3(userMsg3, userGpgPrivKey);

assert.strictEqual(share.from, SignatureShareType.USER);
assert.strictEqual(share.to, SignatureShareType.BITGO);

const parsed = decodeWithCodec(
EddsaMPCv2SignatureShareRound3Input,
JSON.parse(share.share),
'EddsaMPCv2SignatureShareRound3Input'
);
assert.strictEqual(parsed.type, 'round3Input');
assert.ok(parsed.data.msg3.message, 'msg3.message should be set');
assert.ok(parsed.data.msg3.signature, 'msg3.signature should be set');
});
});
Loading
Loading