diff --git a/modules/bitgo/test/v2/unit/internal/tssUtils/ecdsaMPCv2.ts b/modules/bitgo/test/v2/unit/internal/tssUtils/ecdsaMPCv2.ts new file mode 100644 index 0000000000..5960957aeb --- /dev/null +++ b/modules/bitgo/test/v2/unit/internal/tssUtils/ecdsaMPCv2.ts @@ -0,0 +1,428 @@ +import * as assert from 'assert'; +import * as nock from 'nock'; +import * as openpgp from 'openpgp'; +import * as sinon from 'sinon'; + +import { TestableBG, TestBitGo } from '@bitgo/sdk-test'; +import { AddKeychainOptions, BaseCoin, common, ECDSAUtils, Keychain, Wallet } from '@bitgo/sdk-core'; +import { DklsComms, DklsDkg, DklsTypes } from '@bitgo/sdk-lib-mpc'; +import { BitGo, BitgoGPGPublicKey } from '../../../../../src'; + +describe('TSS Ecdsa MPCv2 Utils:', async function () { + const coinName = 'hteth'; + const walletId = '5b34252f1bf349930e34020a00000000'; + const enterpriseId = '6449153a6f6bc20006d66771cdbe15d3'; + let storedUserCommitment2: string; + let storedBackupCommitment2: string; + let storedBitgoCommitment2: string; + + let sandbox: sinon.SinonSandbox; + let bgUrl: string; + let tssUtils: ECDSAUtils.EcdsaMPCv2Utils; + let wallet: Wallet; + let bitgo: TestableBG & BitGo; + let baseCoin: BaseCoin; + let bitGoGgpKey: openpgp.SerializedKeyPair & { + revocationCertificate: string; + }; + let bitgoGpgPrvKey, userGpgPubKey, backupGpgPubKey: { partyId: number; gpgKey: string }; + + beforeEach(async function () { + sandbox = sinon.createSandbox(); + }); + + afterEach(function () { + sandbox.restore(); + }); + + before(async function () { + nock.cleanAll(); + bitGoGgpKey = await openpgp.generateKey({ + userIDs: [ + { + name: 'bitgo', + email: 'bitgo@test.com', + }, + ], + curve: 'secp256k1', + }); + const constants = { + mpc: { + bitgoPublicKey: bitGoGgpKey.publicKey, + }, + }; + + bitgoGpgPrvKey = { + partyId: 2, + gpgKey: bitGoGgpKey.privateKey, + }; + + bitgo = TestBitGo.decorate(BitGo, { env: 'mock' }); + bitgo.initializeTestVars(); + + baseCoin = bitgo.coin(coinName); + + bgUrl = common.Environments[bitgo.getEnv()].uri; + + await nockGetBitgoPublicKeyBasedOnFeatureFlags(coinName, enterpriseId, bitGoGgpKey); + nock(bgUrl).get('/api/v1/client/constants').times(16).reply(200, { ttl: 3600, constants }); + + const walletData = { + id: walletId, + enterprise: enterpriseId, + coin: coinName, + coinSpecific: {}, + multisigType: 'tss', + }; + wallet = new Wallet(bitgo, baseCoin, walletData); + tssUtils = new ECDSAUtils.EcdsaMPCv2Utils(bitgo, baseCoin, wallet); + }); + + after(function () { + nock.cleanAll(); + }); + + describe('TSS key chains', async function () { + it('should generate TSS MPCv2 keys', async function () { + const bitgoSession = new DklsDkg.Dkg(3, 2, 2); + + const round1Nock = await nockKeyGenRound1(bitgoSession, 1); + const round2Nock = await nockKeyGenRound2(bitgoSession, 1); + const round3Nock = await nockKeyGenRound3(bitgoSession, 1); + const addKeyNock = await nockAddKeyChain(coinName, 3); + const params = { + passphrase: 'test', + enterprise: enterpriseId, + originalPasscodeEncryptionCode: '123456', + }; + const { userKeychain, backupKeychain, bitgoKeychain } = await tssUtils.createKeychains(params); + assert.ok(round1Nock.isDone()); + assert.ok(round2Nock.isDone()); + assert.ok(round3Nock.isDone()); + assert.ok(addKeyNock.isDone()); + + assert.ok(userKeychain); + assert.equal(userKeychain.source, 'user'); + assert.ok(userKeychain.commonKeychain); + assert.ok(ECDSAUtils.EcdsaMPCv2Utils.validateCommonKeychainPublicKey(userKeychain.commonKeychain)); + assert.ok(userKeychain.encryptedPrv); + assert.ok(bitgo.decrypt({ input: userKeychain.encryptedPrv, password: params.passphrase })); + + assert.ok(backupKeychain); + assert.equal(backupKeychain.source, 'backup'); + assert.ok(backupKeychain.commonKeychain); + assert.ok(ECDSAUtils.EcdsaMPCv2Utils.validateCommonKeychainPublicKey(backupKeychain.commonKeychain)); + assert.ok(backupKeychain.encryptedPrv); + assert.ok(bitgo.decrypt({ input: backupKeychain.encryptedPrv, password: params.passphrase })); + + assert.ok(bitgoKeychain); + assert.equal(bitgoKeychain.source, 'bitgo'); + }); + + it('should create TSS key chains', async function () { + const nockPromises = [ + nockKeychain({ coin: coinName, keyChain: { id: '1', pub: '1', type: 'tss' }, source: 'user' }), + nockKeychain({ coin: coinName, keyChain: { id: '2', pub: '2', type: 'tss' }, source: 'backup' }), + nockKeychain({ coin: coinName, keyChain: { id: '3', pub: '3', type: 'tss' }, source: 'bitgo' }), + ]; + const [nockedUserKeychain, nockedBackupKeychain, nockedBitGoKeychain] = await Promise.all(nockPromises); + + const bitgoKeychainPromise = tssUtils.createParticipantKeychain(ECDSAUtils.MPCv2IndexesEnum.BITGO, 'test'); + const usersKeychainPromise = tssUtils.createParticipantKeychain( + ECDSAUtils.MPCv2IndexesEnum.USER, + 'test', + Buffer.from('test'), + 'passphrase', + 'test' + ); + const backupKeychainPromise = tssUtils.createParticipantKeychain( + ECDSAUtils.MPCv2IndexesEnum.BACKUP, + 'test', + Buffer.from('test'), + 'passphrase', + 'test' + ); + + const [userKeychain, backupKeychain, bitgoKeychain] = await Promise.all([ + usersKeychainPromise, + backupKeychainPromise, + bitgoKeychainPromise, + ]); + + userKeychain.should.deepEqual(nockedUserKeychain); + backupKeychain.should.deepEqual(nockedBackupKeychain); + bitgoKeychain.should.deepEqual(nockedBitGoKeychain); + }); + }); + + async function nockKeychain( + params: { + coin: string; + keyChain: Keychain; + source: 'user' | 'backup' | 'bitgo'; + }, + times = 1 + ): Promise { + nock(bgUrl) + .post(`/api/v2/${params.coin}/key`, (body) => { + return body.keyType === 'tss' && body.source === params.source; + }) + .times(times) + .reply(200, params.keyChain); + + return params.keyChain; + } + + async function nockGetBitgoPublicKeyBasedOnFeatureFlags( + coin: string, + enterpriseId: string, + bitgoGpgKeyPair: openpgp.SerializedKeyPair + ): Promise { + const bitgoGPGPublicKeyResponse: BitgoGPGPublicKey = { + name: 'irrelevant', + publicKey: bitgoGpgKeyPair.publicKey, + enterpriseId, + }; + nock(bgUrl).get(`/api/v2/${coin}/tss/pubkey`).query({ enterpriseId }).reply(200, bitgoGPGPublicKeyResponse); + + return bitgoGPGPublicKeyResponse; + } + + async function nockKeyGenRound1(bitgoSession: DklsDkg.Dkg, times = 1) { + return nock(bgUrl) + .post(`/api/v2/mpc/generatekey`, (body) => body.round === 'MPCv2-R1') + .times(times) + .reply( + 200, + async ( + uri, + { payload }: { payload: ECDSAUtils.MPCv2KeyGenRound1Request } + ): Promise => { + const { userGpgPublicKey, backupGpgPublicKey, userMsg1, backupMsg1 } = payload; + userGpgPubKey = { + partyId: 0, + gpgKey: userGpgPublicKey, + }; + backupGpgPubKey = { + partyId: 1, + gpgKey: backupGpgPublicKey, + }; + + const bitgoBroadcastMsg1Unsigned = await bitgoSession.initDkg(); + const bitgoMsgs1Signed = await DklsComms.encryptAndAuthOutgoingMessages( + { broadcastMessages: [DklsTypes.serializeBroadcastMessage(bitgoBroadcastMsg1Unsigned)], p2pMessages: [] }, + [], + [bitgoGpgPrvKey] + ); + const bitgoMsg1 = bitgoMsgs1Signed.broadcastMessages.find((m) => m.from === 2); + assert(bitgoMsg1, 'bitgoMsg1 not found'); + + const round1IncomingMsgs = await DklsComms.decryptAndVerifyIncomingMessages( + { + p2pMessages: [], + broadcastMessages: [ + { from: 0, payload: userMsg1 }, + { from: 1, payload: backupMsg1 }, + ], + }, + [userGpgPubKey, backupGpgPubKey], + [bitgoGpgPrvKey] + ); + + const round2Messages = DklsTypes.serializeMessages( + bitgoSession.handleIncomingMessages(DklsTypes.deserializeMessages(round1IncomingMsgs)) + ); + + const round2SignedMessages = await DklsComms.encryptAndAuthOutgoingMessages( + round2Messages, + [userGpgPubKey, backupGpgPubKey], + [bitgoGpgPrvKey] + ); + + const bitgoToUserMsg2 = round2SignedMessages.p2pMessages.find((m) => m.to === 0); + const bitgoToBackupMsg2 = round2SignedMessages.p2pMessages.find((m) => m.to === 1); + assert(bitgoToUserMsg2, 'bitgoToUserMsg2 not found'); + assert(bitgoToBackupMsg2, 'bitgoToBackupMsg2 not found'); + assert(bitgoToUserMsg2.commitment, 'bitgoToUserMsg2.commitment not found'); + + storedBitgoCommitment2 = bitgoToUserMsg2?.commitment; + return { + sessionId: 'testid', + bitgoMsg1: { from: 2, ...bitgoMsg1.payload }, + bitgoToBackupMsg2: { + from: 2, + to: 1, + encryptedMessage: bitgoToBackupMsg2.payload.encryptedMessage, + signature: bitgoToBackupMsg2.payload.signature, + }, + bitgoToUserMsg2: { + from: 2, + to: 0, + encryptedMessage: bitgoToUserMsg2.payload.encryptedMessage, + signature: bitgoToUserMsg2.payload.signature, + }, + }; + } + ); + } + + async function nockKeyGenRound2(bitgoSession: DklsDkg.Dkg, times = 1) { + return nock(bgUrl) + .post(`/api/v2/mpc/generatekey`, (body) => body.round === 'MPCv2-R2') + .times(times) + .reply( + 200, + async ( + uri, + { payload }: { payload: ECDSAUtils.MPCv2KeyGenRound2Request } + ): Promise => { + const { sessionId, userMsg2, backupMsg2, userCommitment2, backupCommitment2 } = payload; + storedUserCommitment2 = userCommitment2; + storedBackupCommitment2 = backupCommitment2; + const round2IncomingMsgs = await DklsComms.decryptAndVerifyIncomingMessages( + { + p2pMessages: [ + { + from: userMsg2.from, + to: userMsg2.to, + payload: { signature: userMsg2.signature, encryptedMessage: userMsg2.encryptedMessage }, + }, + { + from: backupMsg2.from, + to: backupMsg2.to, + payload: { signature: backupMsg2.signature, encryptedMessage: backupMsg2.encryptedMessage }, + }, + ], + broadcastMessages: [], + }, + [userGpgPubKey, backupGpgPubKey], + [bitgoGpgPrvKey] + ); + + const round3Messages = DklsTypes.serializeMessages( + bitgoSession.handleIncomingMessages(DklsTypes.deserializeMessages(round2IncomingMsgs)) + ); + + const round3SignedMessages = await DklsComms.encryptAndAuthOutgoingMessages( + round3Messages, + [userGpgPubKey, backupGpgPubKey], + [bitgoGpgPrvKey] + ); + + const bitgoToUserMsg3 = round3SignedMessages.p2pMessages.find((m) => m.to === 0); + const bitgoToBackupMsg3 = round3SignedMessages.p2pMessages.find((m) => m.to === 1); + assert(bitgoToUserMsg3, 'bitgoToUserMsg3 not found'); + assert(bitgoToBackupMsg3, 'bitgoToBackupMsg3 not found'); + + return { + sessionId, + bitgoCommitment2: storedBitgoCommitment2, + bitgoToUserMsg3: { + from: 2, + to: 0, + encryptedMessage: bitgoToUserMsg3.payload.encryptedMessage, + signature: bitgoToUserMsg3.payload.signature, + }, + bitgoToBackupMsg3: { + from: 2, + to: 1, + encryptedMessage: bitgoToBackupMsg3.payload.encryptedMessage, + signature: bitgoToBackupMsg3.payload.signature, + }, + }; + } + ); + } + + async function nockKeyGenRound3(bitgoSession: DklsDkg.Dkg, times = 1) { + return nock(bgUrl) + .post(`/api/v2/mpc/generatekey`, (body) => body.round === 'MPCv2-R3') + .times(times) + .reply( + 200, + async ( + uri, + { payload }: { payload: ECDSAUtils.MPCv2KeyGenRound3Request } + ): Promise => { + const { sessionId, userMsg3, userMsg4, backupMsg3, backupMsg4 } = payload; + + const round3IncomingMsgs = await DklsComms.decryptAndVerifyIncomingMessages( + { + p2pMessages: [ + { + from: userMsg3.from, + to: userMsg3.to, + payload: { signature: userMsg3.signature, encryptedMessage: userMsg3.encryptedMessage }, + commitment: storedUserCommitment2, + }, + { + from: backupMsg3.from, + to: backupMsg3.to, + payload: { signature: backupMsg3.signature, encryptedMessage: backupMsg3.encryptedMessage }, + commitment: storedBackupCommitment2, + }, + ], + broadcastMessages: [], + }, + [userGpgPubKey, backupGpgPubKey], + [bitgoGpgPrvKey] + ); + + const round4Messages = DklsTypes.serializeMessages( + bitgoSession.handleIncomingMessages(DklsTypes.deserializeMessages(round3IncomingMsgs)) + ); + const round4SignedMessages = await DklsComms.encryptAndAuthOutgoingMessages( + round4Messages, + [], + [bitgoGpgPrvKey] + ); + const bitgoMsg4 = round4SignedMessages.broadcastMessages.find((m) => m.from === 2); + assert(bitgoMsg4, 'bitgoMsg4 not found'); + + const round4IncomingMsgs = await DklsComms.decryptAndVerifyIncomingMessages( + { + p2pMessages: [], + broadcastMessages: [ + { + from: userMsg4.from, + + payload: { signature: userMsg4.signature, message: userMsg4.message }, + }, + { + from: backupMsg4.from, + + payload: { signature: backupMsg4.signature, message: backupMsg4.message }, + }, + ], + }, + [userGpgPubKey, backupGpgPubKey], + [] + ); + bitgoSession.handleIncomingMessages(DklsTypes.deserializeMessages(round4IncomingMsgs)); + const keyShare = bitgoSession.getKeyShare(); + const commonKeychain = DklsTypes.getCommonKeychain(keyShare); + + return { + sessionId, + commonKeychain, + bitgoMsg4: { from: 2, ...bitgoMsg4.payload }, + }; + } + ); + } + + async function nockAddKeyChain(coin: string, times = 1) { + return nock('https://bitgo.fakeurl') + .post(`/api/v2/${coin}/key`, (body) => body.keyType === 'tss') + .times(times) + .reply(200, async (uri, requestBody: AddKeychainOptions) => { + return { + id: requestBody.source, + source: requestBody.source, + keyType: requestBody.keyType, + commonKeychain: requestBody.commonKeychain, + encryptedPrv: requestBody.encryptedPrv, + }; + }); + } +}); diff --git a/modules/bitgo/test/v2/unit/wallets.ts b/modules/bitgo/test/v2/unit/wallets.ts index 80b676ce4e..05811ab487 100644 --- a/modules/bitgo/test/v2/unit/wallets.ts +++ b/modules/bitgo/test/v2/unit/wallets.ts @@ -1,8 +1,12 @@ // // Tests for Wallets // - +import * as assert from 'assert'; import * as nock from 'nock'; +import * as sinon from 'sinon'; +import * as should from 'should'; +import * as _ from 'lodash'; +import { TestBitGo } from '@bitgo/sdk-test'; import { BlsUtils, common, @@ -12,11 +16,7 @@ import { KeychainsTriplet, GenerateWalletOptions, } from '@bitgo/sdk-core'; -import * as _ from 'lodash'; -import { TestBitGo } from '@bitgo/sdk-test'; import { BitGo } from '../../../src/bitgo'; -import * as sinon from 'sinon'; -import * as should from 'should'; describe('V2 Wallets:', function () { const bitgo = TestBitGo.decorate(BitGo, { env: 'mock' }); @@ -702,6 +702,105 @@ describe('V2 Wallets:', function () { }); }); + describe('Generate TSS MPCv2 wallet:', async function () { + it('should create a new TSS MPCv2 wallet', async function () { + const hteth = bitgo.coin('hteth'); + const sandbox = sinon.createSandbox(); + const stubbedKeychainsTriplet: KeychainsTriplet = { + userKeychain: { + id: '1', + commonKeychain: 'userPub', + type: 'tss', + source: 'user', + }, + backupKeychain: { + id: '2', + commonKeychain: 'userPub', + type: 'tss', + source: 'backup', + }, + bitgoKeychain: { + id: '3', + commonKeychain: 'userPub', + type: 'tss', + source: 'bitgo', + }, + }; + sandbox.stub(ECDSAUtils.EcdsaMPCv2Utils.prototype, 'createKeychains').resolves(stubbedKeychainsTriplet); + + const walletNock = nock('https://bitgo.fakeurl').post('/api/v2/hteth/wallet').reply(200); + + const wallets = new Wallets(bitgo, hteth); + + await wallets.generateWallet({ + label: 'tss wallet', + passphrase: 'tss password', + multisigType: 'tss', + enterprise: 'enterprise', + passcodeEncryptionCode: 'originalPasscodeEncryptionCode', + walletVersion: 5, + }); + + walletNock.isDone().should.be.true(); + sandbox.verifyAndRestore(); + }); + + it('should throw for an unsupported coin', async function () { + const tpolygon = bitgo.coin('tpolygon'); + const wallets = new Wallets(bitgo, tpolygon); + + await assert.rejects( + async () => { + await wallets.generateWallet({ + label: 'tss wallet', + passphrase: 'tss password', + multisigType: 'tss', + enterprise: 'enterprise', + passcodeEncryptionCode: 'originalPasscodeEncryptionCode', + walletVersion: 5, + }); + }, + { message: 'coin polygon does not support TSS MPCv2 at this time' } + ); + }); + + it('should throw for a cold wallet', async function () { + const hteth = bitgo.coin('hteth'); + const wallets = new Wallets(bitgo, hteth); + + await assert.rejects( + async () => { + await wallets.generateWallet({ + label: 'tss wallet', + multisigType: 'tss', + enterprise: 'enterprise', + walletVersion: 5, + type: 'cold', + }); + }, + { message: 'EVM TSS MPCv2 wallets are not supported for cold wallets' } + ); + }); + + it('should throw for a custodial wallet', async function () { + const hteth = bitgo.coin('hteth'); + const wallets = new Wallets(bitgo, hteth); + + await assert.rejects( + async () => { + await wallets.generateWallet({ + label: 'tss wallet', + multisigType: 'tss', + enterprise: 'enterprise', + walletVersion: 5, + type: 'custodial', + }); + }, + { message: 'EVM TSS MPCv2 wallets are not supported for custodial wallets' } + ); + }); + }); + describe('Generate BLS-DKG wallet:', function () { const eth2 = bitgo.coin('eth2'); diff --git a/modules/sdk-coin-eth/src/eth.ts b/modules/sdk-coin-eth/src/eth.ts index 967431630c..4a28ea20c5 100644 --- a/modules/sdk-coin-eth/src/eth.ts +++ b/modules/sdk-coin-eth/src/eth.ts @@ -80,6 +80,11 @@ export class Eth extends AbstractEthLikeNewCoins { return true; } + /** @inheritDoc */ + supportsMPCv2(): boolean { + return true; + } + getMPCAlgorithm(): MPCAlgorithm { return 'ecdsa'; } diff --git a/modules/sdk-core/src/bitgo/baseCoin/baseCoin.ts b/modules/sdk-core/src/bitgo/baseCoin/baseCoin.ts index c29110bbe1..366b8304d1 100644 --- a/modules/sdk-core/src/bitgo/baseCoin/baseCoin.ts +++ b/modules/sdk-core/src/bitgo/baseCoin/baseCoin.ts @@ -172,6 +172,14 @@ export abstract class BaseCoin implements IBaseCoin { return false; } + /** + * Flag indicating if this coin supports MPCv2 wallets. + * @returns {boolean} True if MPCv2 Wallets can be created for this coin + */ + supportsMPCv2(): boolean { + return false; + } + /** * Flag indicating if the coin supports deriving a key with a seed (keyID) * to the user/backup keys. diff --git a/modules/sdk-core/src/bitgo/baseCoin/iBaseCoin.ts b/modules/sdk-core/src/bitgo/baseCoin/iBaseCoin.ts index c733057063..2c6f49046a 100644 --- a/modules/sdk-core/src/bitgo/baseCoin/iBaseCoin.ts +++ b/modules/sdk-core/src/bitgo/baseCoin/iBaseCoin.ts @@ -464,6 +464,7 @@ export interface IBaseCoin { allowsAccountConsolidations(): boolean; getTokenEnablementConfig(): TokenEnablementConfig; supportsTss(): boolean; + supportsMPCv2(): boolean; supportsDeriveKeyWithSeed(): boolean; isEVM(): boolean; supportsBlsDkg(): boolean; diff --git a/modules/sdk-core/src/bitgo/keychain/iKeychains.ts b/modules/sdk-core/src/bitgo/keychain/iKeychains.ts index 77752ac30f..299f92ba4f 100644 --- a/modules/sdk-core/src/bitgo/keychain/iKeychains.ts +++ b/modules/sdk-core/src/bitgo/keychain/iKeychains.ts @@ -141,6 +141,7 @@ export interface CreateMpcOptions { originalPasscodeEncryptionCode?: string; enterprise?: string; backupProvider?: BackupProvider; + walletVersion?: number; } export interface GetKeysForSigningOptions { diff --git a/modules/sdk-core/src/bitgo/keychain/keychains.ts b/modules/sdk-core/src/bitgo/keychain/keychains.ts index bac469afe2..3fc0cdce9a 100644 --- a/modules/sdk-core/src/bitgo/keychain/keychains.ts +++ b/modules/sdk-core/src/bitgo/keychain/keychains.ts @@ -296,7 +296,12 @@ export class Keychains implements IKeychains { let MpcUtils; switch (params.multisigType) { case 'tss': - MpcUtils = this.baseCoin.getMPCAlgorithm() === 'ecdsa' ? ECDSAUtils.EcdsaUtils : EDDSAUtils.default; + MpcUtils = + this.baseCoin.getMPCAlgorithm() === 'eddsa' + ? EDDSAUtils.default + : params.walletVersion === 5 + ? ECDSAUtils.EcdsaMPCv2Utils + : ECDSAUtils.EcdsaUtils; break; case 'blsdkg': if (_.isUndefined(params.passphrase)) { diff --git a/modules/sdk-core/src/bitgo/utils/tss/ecdsa/base.ts b/modules/sdk-core/src/bitgo/utils/tss/ecdsa/base.ts new file mode 100644 index 0000000000..aeb6105370 --- /dev/null +++ b/modules/sdk-core/src/bitgo/utils/tss/ecdsa/base.ts @@ -0,0 +1,73 @@ +import * as openpgp from 'openpgp'; +import { ec } from 'elliptic'; + +import { IBaseCoin } from '../../../baseCoin'; +import baseTSSUtils from '../baseTSSUtils'; +import { KeyShare } from './types'; +import { BackupGpgKey } from '../baseTypes'; +import { generateGPGKeyPair, getBitgoGpgPubKey, getTrustGpgPubKey } from '../../opengpgUtils'; +import { BitGoBase } from '../../../bitgoBase'; +import { IWallet } from '../../../wallet'; + +/** @inheritdoc */ +export class BaseEcdsaUtils extends baseTSSUtils { + // We do not have full support for 3-party verification (w/ external source) of key shares and signature shares. There is no 3rd party key service support with this release. + protected bitgoPublicGpgKey: openpgp.Key; + + constructor(bitgo: BitGoBase, baseCoin: IBaseCoin, wallet?: IWallet) { + super(bitgo, baseCoin, wallet); + this.setBitgoGpgPubKey(bitgo); + } + + private async setBitgoGpgPubKey(bitgo) { + this.bitgoPublicGpgKey = await getBitgoGpgPubKey(bitgo); + } + + async getBitgoPublicGpgKey(): Promise { + if (!this.bitgoPublicGpgKey) { + // retry getting bitgo's gpg key + await this.setBitgoGpgPubKey(this.bitgo); + if (!this.bitgoPublicGpgKey) { + throw new Error("Failed to get Bitgo's gpg key"); + } + } + + return this.bitgoPublicGpgKey; + } + + /** + * Gets backup pub gpg key string + * if a third party provided then get from trust + * @param isThirdPartyBackup + */ + async getBackupGpgPubKey(isThirdPartyBackup = false): Promise { + return isThirdPartyBackup ? getTrustGpgPubKey(this.bitgo) : generateGPGKeyPair('secp256k1'); + } + + /** + * util function that checks that a commonKeychain is valid and can ultimately resolve to a valid public key + * @param commonKeychain - a user uploaded commonKeychain string + * @throws if the commonKeychain is invalid length or invalid format + */ + + static validateCommonKeychainPublicKey(commonKeychain: string) { + const pub = BaseEcdsaUtils.getPublicKeyFromCommonKeychain(commonKeychain); + const secp256k1 = new ec('secp256k1'); + const key = secp256k1.keyFromPublic(pub, 'hex'); + return key.getPublic().encode('hex', false).slice(2); + } + + /** + * Gets the common public key from commonKeychain. + * + * @param {String} commonKeychain common key chain between n parties + * @returns {string} encoded public key + */ + static getPublicKeyFromCommonKeychain(commonKeychain: string): string { + if (commonKeychain.length !== 130) { + throw new Error(`Invalid commonKeychain length, expected 130, got ${commonKeychain.length}`); + } + const commonPubHexStr = commonKeychain.slice(0, 66); + return commonPubHexStr; + } +} diff --git a/modules/sdk-core/src/bitgo/utils/tss/ecdsa/ecdsa.ts b/modules/sdk-core/src/bitgo/utils/tss/ecdsa/ecdsa.ts index 53616db303..49252d3108 100644 --- a/modules/sdk-core/src/bitgo/utils/tss/ecdsa/ecdsa.ts +++ b/modules/sdk-core/src/bitgo/utils/tss/ecdsa/ecdsa.ts @@ -2,7 +2,6 @@ import assert from 'assert'; import { Buffer } from 'buffer'; import { Key, SerializedKeyPair } from 'openpgp'; import * as openpgp from 'openpgp'; -import { ec } from 'elliptic'; import { Hash } from 'crypto'; import { EcdsaPaillierProof, EcdsaRangeProof, EcdsaTypes, hexToBigInt, minModulusBitLength } from '@bitgo/sdk-lib-mpc'; @@ -11,8 +10,7 @@ import { bip32 } from '@bitgo/utxo-lib'; import { ECDSA, Ecdsa } from '../../../../account-lib/mpc/tss'; import { AddKeychainOptions, ApiKeyShare, CreateBackupOptions, Keychain, KeyType } from '../../../keychain'; import ECDSAMethods, { ECDSAMethodTypes } from '../../../tss/ecdsa'; -import { IBaseCoin, KeychainsTriplet } from '../../../baseCoin'; -import baseTSSUtils from '../baseTSSUtils'; +import { KeychainsTriplet } from '../../../baseCoin'; import { BitGoProofSignatures, CreateEcdsaBitGoKeychainParams, @@ -22,7 +20,6 @@ import { KeyShare, } from './types'; import { - BackupGpgKey, BackupKeyShare, BitgoHeldBackupKeyShare, CustomKShareGeneratingFunction, @@ -36,9 +33,9 @@ import { } from '../baseTypes'; import { getTxRequest } from '../../../tss'; import { AShare, DShare, EncryptedNShare, OShare, SendShareType, SShare, WShare } from '../../../tss/ecdsa/types'; -import { createShareProof, generateGPGKeyPair, getBitgoGpgPubKey, getTrustGpgPubKey } from '../../opengpgUtils'; +import { createShareProof, generateGPGKeyPair, getBitgoGpgPubKey } from '../../opengpgUtils'; import { BitGoBase } from '../../../bitgoBase'; -import { BackupProvider, IWallet } from '../../../wallet'; +import { BackupProvider } from '../../../wallet'; import { buildNShareFromAPIKeyShare, getParticipantFromIndex, verifyWalletSignature } from '../../../tss/ecdsa/ecdsa'; import { signMessageWithDerivedEcdhKey, verifyEcdhSignature } from '../../../ecdh'; import { getTxRequestChallenge } from '../../../tss/common'; @@ -48,49 +45,12 @@ import { TssEcdsaStep2ReturnMessage, TxRequestChallengeResponse, } from '../../../tss/types'; +import { BaseEcdsaUtils } from './base'; const encryptNShare = ECDSAMethods.encryptNShare; /** @inheritdoc */ -export class EcdsaUtils extends baseTSSUtils { - // We do not have full support for 3-party verification (w/ external source) of key shares and signature shares. There is no 3rd party key service support with this release. - private bitgoPublicGpgKey: openpgp.Key | undefined = undefined; - - constructor(bitgo: BitGoBase, baseCoin: IBaseCoin, wallet?: IWallet) { - super(bitgo, baseCoin, wallet); - this.setBitgoGpgPubKey(bitgo); - } - - private async setBitgoGpgPubKey(bitgo) { - this.bitgoPublicGpgKey = await getBitgoGpgPubKey(bitgo); - } - - async getBitgoPublicGpgKey(): Promise { - if (!this.bitgoPublicGpgKey) { - // retry getting bitgo's gpg key - await this.setBitgoGpgPubKey(this.bitgo); - if (!this.bitgoPublicGpgKey) { - throw new Error("Failed to get Bitgo's gpg key"); - } - } - - return this.bitgoPublicGpgKey; - } - - /** - * Gets the common public key from commonKeychain. - * - * @param {String} commonKeychain common key chain between n parties - * @returns {string} encoded public key - */ - static getPublicKeyFromCommonKeychain(commonKeychain: string): string { - if (commonKeychain.length !== 130) { - throw new Error(`Invalid commonKeychain length, expected 130, got ${commonKeychain.length}`); - } - const commonPubHexStr = commonKeychain.slice(0, 66); - return commonPubHexStr; - } - +export class EcdsaUtils extends BaseEcdsaUtils { async finalizeBitgoHeldBackupKeyShare( keyId: string, commonKeychain: string, @@ -228,15 +188,6 @@ export class EcdsaUtils extends baseTSSUtils { return backupKeyShare; } - /** - * Gets backup pub gpg key string - * if a third party provided then get from trust - * @param isThirdPartyBackup - */ - async getBackupGpgPubKey(isThirdPartyBackup = false): Promise { - return isThirdPartyBackup ? getTrustGpgPubKey(this.bitgo) : generateGPGKeyPair('secp256k1'); - } - createUserKeychain({ userGpgKey, backupGpgKey, @@ -1409,17 +1360,4 @@ export class EcdsaUtils extends baseTSSUtils { .send(body) .result(); } - - /** - * util function that checks that a commonKeychain is valid and can ultimately resolve to a valid public key - * @param commonKeychain - a user uploaded commonKeychain string - * @throws if the commonKeychain is invalid length or invalid format - */ - - static validateCommonKeychainPublicKey(commonKeychain: string) { - const pub = EcdsaUtils.getPublicKeyFromCommonKeychain(commonKeychain); - const secp256k1 = new ec('secp256k1'); - const key = secp256k1.keyFromPublic(pub, 'hex'); - return key.getPublic().encode('hex', false).slice(2); - } } diff --git a/modules/sdk-core/src/bitgo/utils/tss/ecdsa/ecdsaMPCv2.ts b/modules/sdk-core/src/bitgo/utils/tss/ecdsa/ecdsaMPCv2.ts new file mode 100644 index 0000000000..aa05f92178 --- /dev/null +++ b/modules/sdk-core/src/bitgo/utils/tss/ecdsa/ecdsaMPCv2.ts @@ -0,0 +1,479 @@ +import assert from 'assert'; +import { DklsDkg, DklsTypes, DklsComms } from '@bitgo/sdk-lib-mpc'; + +import { Keychain, KeyType } from '../../../keychain'; +import { KeychainsTriplet } from '../../../baseCoin'; +import { generateGPGKeyPair } from '../../opengpgUtils'; +import { BaseEcdsaUtils } from './base'; +import { + GenerateMPCv2KeyRequestBody, + GenerateMPCv2KeyRequestResponse, + KeyGenStateRound, + KeyGenStateRoundEnum, + KeyGenTypeEnum, + MPCv2BroadcastMessage, + MPCv2IndexesEnum, + MPCv2KeyGenRound1Response, + MPCv2KeyGenRound2Response, + MPCv2KeyGenRound3Response, + MPCv2P2PMessage, +} from './typesMPCv2'; + +export class EcdsaMPCv2Utils extends BaseEcdsaUtils { + /** @inheritdoc */ + async createKeychains(params: { + passphrase: string; + enterprise: string; + originalPasscodeEncryptionCode?: string; + }): Promise { + const m = 2; + const n = 3; + const userSession = new DklsDkg.Dkg(n, m, MPCv2IndexesEnum.USER); + const backupSession = new DklsDkg.Dkg(n, m, MPCv2IndexesEnum.BACKUP); + const userGpgKey = await generateGPGKeyPair('secp256k1'); + const backupGpgKey = await generateGPGKeyPair('secp256k1'); + + // Get the BitGo public key based on user/enterprise feature flags + // If it doesn't work, use the default public key from the constants + const bitgoPublicGpgKey = ( + (await this.getBitgoGpgPubkeyBasedOnFeatureFlags(params.enterprise)) ?? this.bitgoPublicGpgKey + ).armor(); + + const userGpgPrvKey: DklsTypes.PartyGpgKey = { + partyId: MPCv2IndexesEnum.USER, + gpgKey: userGpgKey.privateKey, + }; + const backupGpgPrvKey: DklsTypes.PartyGpgKey = { + partyId: MPCv2IndexesEnum.BACKUP, + gpgKey: backupGpgKey.privateKey, + }; + const bitgoGpgPubKey: DklsTypes.PartyGpgKey = { + partyId: MPCv2IndexesEnum.BITGO, + gpgKey: bitgoPublicGpgKey, + }; + + // #region round 1 + const userRound1BroadcastMsg = await userSession.initDkg(); + const backupRound1BroadcastMsg = await backupSession.initDkg(); + + const round1SerializedMessages = DklsTypes.serializeMessages({ + broadcastMessages: [userRound1BroadcastMsg, backupRound1BroadcastMsg], + p2pMessages: [], + }); + const round1Messages = await DklsComms.encryptAndAuthOutgoingMessages( + round1SerializedMessages, + [bitgoGpgPubKey], + [userGpgPrvKey, backupGpgPrvKey] + ); + + const { sessionId, bitgoMsg1, bitgoToBackupMsg2, bitgoToUserMsg2 } = await this.sendKeyGenerationRound1( + params.enterprise, + userGpgKey.publicKey, + backupGpgKey.publicKey, + round1Messages + ); + // #endregion + + // #region round 2 + const bitgoRound1BroadcastMessages = await DklsComms.decryptAndVerifyIncomingMessages( + { p2pMessages: [], broadcastMessages: [this.formatBitgoBroadcastMessage(bitgoMsg1)] }, + [bitgoGpgPubKey], + [userGpgPrvKey, backupGpgPrvKey] + ); + const bitgoRound1BroadcastMsg = bitgoRound1BroadcastMessages.broadcastMessages.find( + (m) => m.from === MPCv2IndexesEnum.BITGO + ); + assert(bitgoRound1BroadcastMsg, 'BitGo message 1 not found in broadcast messages'); + + const userRound2P2PMessages = userSession.handleIncomingMessages({ + p2pMessages: [], + broadcastMessages: [DklsTypes.deserializeBroadcastMessage(bitgoRound1BroadcastMsg), backupRound1BroadcastMsg], + }); + + const userToBitgoMsg2 = userRound2P2PMessages.p2pMessages.find( + (m) => m.from === MPCv2IndexesEnum.USER && m.to === MPCv2IndexesEnum.BITGO + ); + assert(userToBitgoMsg2, 'User message 2 not found in P2P messages'); + const serializedUserToBitgoMsg2 = DklsTypes.serializeP2PMessage(userToBitgoMsg2); + + const backupRound2P2PMessages = backupSession.handleIncomingMessages({ + p2pMessages: [], + broadcastMessages: [userRound1BroadcastMsg, DklsTypes.deserializeBroadcastMessage(bitgoRound1BroadcastMsg)], + }); + const serializedBackupToBitgoMsg2 = DklsTypes.serializeMessages(backupRound2P2PMessages).p2pMessages.find( + (m) => m.from === MPCv2IndexesEnum.BACKUP && m.to === MPCv2IndexesEnum.BITGO + ); + assert(serializedBackupToBitgoMsg2, 'Backup message 2 not found in P2P messages'); + + const round2Messages = await DklsComms.encryptAndAuthOutgoingMessages( + { p2pMessages: [serializedUserToBitgoMsg2, serializedBackupToBitgoMsg2], broadcastMessages: [] }, + [bitgoGpgPubKey], + [userGpgPrvKey, backupGpgPrvKey] + ); + + const { + sessionId: sessionIdRound2, + bitgoCommitment2, + bitgoToUserMsg3, + bitgoToBackupMsg3, + } = await this.sendKeyGenerationRound2(params.enterprise, sessionId, round2Messages); + // #endregion + + // #region round 3 + assert.equal(sessionId, sessionIdRound2, 'Round 1 and 2 Session IDs do not match'); + const decryptedBitgoToUserRound2Msgs = await DklsComms.decryptAndVerifyIncomingMessages( + { p2pMessages: [this.formatP2PMessage(bitgoToUserMsg2)], broadcastMessages: [] }, + [bitgoGpgPubKey], + [userGpgPrvKey] + ); + const serializedBitgoToUserRound2Msg = decryptedBitgoToUserRound2Msgs.p2pMessages.find( + (m) => m.from === MPCv2IndexesEnum.BITGO && m.to === MPCv2IndexesEnum.USER + ); + assert(serializedBitgoToUserRound2Msg, 'BitGo to User message 2 not found in P2P messages'); + const bitgoToUserRound2Msg = DklsTypes.deserializeP2PMessage(serializedBitgoToUserRound2Msg); + + const decryptedBitgoToBackupRound2Msg = await DklsComms.decryptAndVerifyIncomingMessages( + { p2pMessages: [this.formatP2PMessage(bitgoToBackupMsg2)], broadcastMessages: [] }, + [bitgoGpgPubKey], + [backupGpgPrvKey] + ); + const serializedBitgoToBackupRound2Msg = decryptedBitgoToBackupRound2Msg.p2pMessages.find( + (m) => m.from === MPCv2IndexesEnum.BITGO && m.to === MPCv2IndexesEnum.BACKUP + ); + assert(serializedBitgoToBackupRound2Msg, 'BitGo to Backup message 2 not found in P2P messages'); + const bitgoToBackupRound2Msg = DklsTypes.deserializeP2PMessage(serializedBitgoToBackupRound2Msg); + + const userToBackupMsg2 = userRound2P2PMessages.p2pMessages.find( + (m) => m.from === MPCv2IndexesEnum.USER && m.to === MPCv2IndexesEnum.BACKUP + ); + assert(userToBackupMsg2, 'User to Backup message 2 not found in P2P messages'); + + const backupToUserMsg2 = backupRound2P2PMessages.p2pMessages.find( + (m) => m.from === MPCv2IndexesEnum.BACKUP && m.to === MPCv2IndexesEnum.USER + ); + assert(backupToUserMsg2, 'Backup to User message 2 not found in P2P messages'); + + const userRound3Messages = userSession.handleIncomingMessages({ + broadcastMessages: [], + p2pMessages: [bitgoToUserRound2Msg, backupToUserMsg2], + }); + const userToBackupMsg3 = userRound3Messages.p2pMessages.find( + (m) => m.from === MPCv2IndexesEnum.USER && m.to === MPCv2IndexesEnum.BACKUP + ); + assert(userToBackupMsg3, 'User to Backup message 3 not found in P2P messages'); + const userToBitgoMsg3 = userRound3Messages.p2pMessages.find( + (m) => m.from === MPCv2IndexesEnum.USER && m.to === MPCv2IndexesEnum.BITGO + ); + assert(userToBitgoMsg3, 'User to Bitgo message 3 not found in P2P messages'); + const serializedUserToBitgoMsg3 = DklsTypes.serializeP2PMessage(userToBitgoMsg3); + + const backupRound3Messages = backupSession.handleIncomingMessages({ + broadcastMessages: [], + p2pMessages: [bitgoToBackupRound2Msg, userToBackupMsg2], + }); + + const backupToUserMsg3 = backupRound3Messages.p2pMessages.find((m) => m.from === 1 && m.to === 0); + assert(backupToUserMsg3, 'Backup to User message 3 not found in P2P messages'); + const backupToBitgoMsg3 = backupRound3Messages.p2pMessages.find( + (m) => m.from === MPCv2IndexesEnum.BACKUP && m.to === MPCv2IndexesEnum.BITGO + ); + assert(backupToBitgoMsg3, 'Backup to Bitgo message 3 not found in P2P messages'); + const serializedBackupToBitgoMsg3 = DklsTypes.serializeP2PMessage(backupToBitgoMsg3); + + const decryptedBitgoToUserRound3Messages = await DklsComms.decryptAndVerifyIncomingMessages( + { broadcastMessages: [], p2pMessages: [this.formatP2PMessage(bitgoToUserMsg3, bitgoCommitment2)] }, + [bitgoGpgPubKey], + [userGpgPrvKey] + ); + const serializedBitgoToUserRound3Msg = decryptedBitgoToUserRound3Messages.p2pMessages.find( + (m) => m.from === MPCv2IndexesEnum.BITGO && m.to === MPCv2IndexesEnum.USER + ); + assert(serializedBitgoToUserRound3Msg, 'BitGo to User message 3 not found in P2P messages'); + const bitgoToUserRound3Msg = DklsTypes.deserializeP2PMessage(serializedBitgoToUserRound3Msg); + + const decryptedBitgoToBackupRound3Messages = await DklsComms.decryptAndVerifyIncomingMessages( + { broadcastMessages: [], p2pMessages: [this.formatP2PMessage(bitgoToBackupMsg3, bitgoCommitment2)] }, + [bitgoGpgPubKey], + [backupGpgPrvKey] + ); + const serializedBitgoToBackupRound3Msg = decryptedBitgoToBackupRound3Messages.p2pMessages.find( + (m) => m.from === MPCv2IndexesEnum.BITGO && m.to === MPCv2IndexesEnum.BACKUP + ); + assert(serializedBitgoToBackupRound3Msg, 'BitGo to Backup message 3 not found in P2P messages'); + const bitgoToBackupRound3Msg = DklsTypes.deserializeP2PMessage(serializedBitgoToBackupRound3Msg); + + const userRound4Messages = userSession.handleIncomingMessages({ + p2pMessages: [backupToUserMsg3, bitgoToUserRound3Msg], + broadcastMessages: [], + }); + + const userRound4BroadcastMsg = userRound4Messages.broadcastMessages.find((m) => m.from === MPCv2IndexesEnum.USER); + assert(userRound4BroadcastMsg, 'User message 4 not found in broadcast messages'); + const serializedUserRound4BroadcastMsg = DklsTypes.serializeBroadcastMessage(userRound4BroadcastMsg); + + const backupRound4Messages = backupSession.handleIncomingMessages({ + p2pMessages: [userToBackupMsg3, bitgoToBackupRound3Msg], + broadcastMessages: [], + }); + const backupRound4BroadcastMsg = backupRound4Messages.broadcastMessages.find( + (m) => m.from === MPCv2IndexesEnum.BACKUP + ); + assert(backupRound4BroadcastMsg, 'Backup message 4 not found in broadcast messages'); + const serializedBackupRound4BroadcastMsg = DklsTypes.serializeBroadcastMessage(backupRound4BroadcastMsg); + + const round3Messages = await DklsComms.encryptAndAuthOutgoingMessages( + { + p2pMessages: [serializedUserToBitgoMsg3, serializedBackupToBitgoMsg3], + broadcastMessages: [serializedUserRound4BroadcastMsg, serializedBackupRound4BroadcastMsg], + }, + [bitgoGpgPubKey], + [userGpgPrvKey, backupGpgPrvKey] + ); + + const { + sessionId: sessionIdRound3, + bitgoMsg4, + commonKeychain: bitgoCommonKeychain, + } = await this.sendKeyGenerationRound3(params.enterprise, sessionId, round3Messages); + + // #endregion + + // #region keychain creation + assert.equal(sessionId, sessionIdRound3, 'Round 1 and 3 Session IDs do not match'); + const bitgoRound4BroadcastMessages = DklsTypes.deserializeMessages( + await DklsComms.decryptAndVerifyIncomingMessages( + { p2pMessages: [], broadcastMessages: [this.formatBitgoBroadcastMessage(bitgoMsg4)] }, + [bitgoGpgPubKey], + [] + ) + ).broadcastMessages; + const bitgoRound4BroadcastMsg = bitgoRound4BroadcastMessages.find((m) => m.from === MPCv2IndexesEnum.BITGO); + + assert(bitgoRound4BroadcastMsg, 'BitGo message 4 not found in broadcast messages'); + userSession.handleIncomingMessages({ + p2pMessages: [], + broadcastMessages: [bitgoRound4BroadcastMsg, backupRound4BroadcastMsg], + }); + + backupSession.handleIncomingMessages({ + p2pMessages: [], + broadcastMessages: [bitgoRound4BroadcastMsg, userRound4BroadcastMsg], + }); + + const userPrivateMaterial = userSession.getKeyShare(); + const backupPrivateMaterial = backupSession.getKeyShare(); + + const userCommonKeychain = DklsTypes.getCommonKeychain(userPrivateMaterial); + const backupCommonKeychain = DklsTypes.getCommonKeychain(backupPrivateMaterial); + + assert.equal(bitgoCommonKeychain, userCommonKeychain, 'User and Bitgo Common keychains do not match'); + assert.equal(bitgoCommonKeychain, backupCommonKeychain, 'Backup and Bitgo Common keychains do not match'); + + const userKeychainPromise = this.addUserKeychain( + bitgoCommonKeychain, + userPrivateMaterial, + params.passphrase, + params.originalPasscodeEncryptionCode + ); + const backupKeychainPromise = this.addBackupKeychain( + bitgoCommonKeychain, + userPrivateMaterial, + params.passphrase, + params.originalPasscodeEncryptionCode + ); + const bitgoKeychainPromise = this.addBitgoKeychain(bitgoCommonKeychain); + + const [userKeychain, backupKeychain, bitgoKeychain] = await Promise.all([ + userKeychainPromise, + backupKeychainPromise, + bitgoKeychainPromise, + ]); + // #endregion + + return { + userKeychain, + backupKeychain, + bitgoKeychain, + }; + } + + // #region keychain utils + async createParticipantKeychain( + participantIndex: MPCv2IndexesEnum, + commonKeychain: string, + privateMaterial?: Buffer, + passphrase?: string, + originalPasscodeEncryptionCode?: string + ): Promise { + let source: string; + let encryptedPrv: string | undefined = undefined; + switch (participantIndex) { + case MPCv2IndexesEnum.USER: + case MPCv2IndexesEnum.BACKUP: + source = participantIndex === MPCv2IndexesEnum.USER ? 'user' : 'backup'; + assert(privateMaterial, `Private material is required for ${source} keychain`); + assert(passphrase, `Passphrase is required for ${source} keychain`); + encryptedPrv = this.bitgo.encrypt({ + input: privateMaterial.toString('base64'), + password: passphrase, + }); + break; + case MPCv2IndexesEnum.BITGO: + source = 'bitgo'; + break; + default: + throw new Error('Invalid participant index'); + } + + const recipientKeychainParams = { + source, + keyType: 'tss' as KeyType, + commonKeychain, + encryptedPrv, + originalPasscodeEncryptionCode, + }; + + const keychains = this.baseCoin.keychains(); + return keychains.add(recipientKeychainParams); + } + + private async addUserKeychain( + commonKeychain: string, + privateMaterial: Buffer, + passphrase: string, + originalPasscodeEncryptionCode?: string + ): Promise { + return this.createParticipantKeychain( + MPCv2IndexesEnum.USER, + commonKeychain, + privateMaterial, + passphrase, + originalPasscodeEncryptionCode + ); + } + + private async addBackupKeychain( + commonKeychain: string, + privateMaterial: Buffer, + passphrase: string, + originalPasscodeEncryptionCode?: string + ): Promise { + return this.createParticipantKeychain( + MPCv2IndexesEnum.BACKUP, + commonKeychain, + privateMaterial, + passphrase, + originalPasscodeEncryptionCode + ); + } + + private async addBitgoKeychain(commonKeychain: string): Promise { + return this.createParticipantKeychain(MPCv2IndexesEnum.BITGO, commonKeychain); + } + // #endregion + + // #region generate key request utils + private async sendKeyGenerationRequest( + enterprise: string, + round: KeyGenStateRound, + payload: GenerateMPCv2KeyRequestBody + ): Promise { + return this.bitgo + .post(this.bitgo.url('/mpc/generatekey', 2)) + .send({ enterprise, type: KeyGenTypeEnum.MPCv2, round, payload }) + .result(); + } + + private async sendKeyGenerationRound1( + enterprise: string, + userGpgPublicKey: string, + backupGpgPublicKey: string, + payload: DklsTypes.AuthEncMessages + ): Promise { + const userMsg1 = payload.broadcastMessages.find((m) => m.from === 0)?.payload; + assert(userMsg1, 'User message 1 not found in broadcast messages'); + const backupMsg1 = payload.broadcastMessages.find((m) => m.from === 1)?.payload; + assert(backupMsg1, 'Backup message 1 not found in broadcast messages'); + + return this.sendKeyGenerationRequest(enterprise, KeyGenStateRoundEnum['MPCv2-R1'], { + userGpgPublicKey, + backupGpgPublicKey, + userMsg1: { from: 0, ...userMsg1 }, + backupMsg1: { from: 1, ...backupMsg1 }, + }); + } + + private async sendKeyGenerationRound2( + enterprise: string, + sessionId: string, + payload: DklsTypes.AuthEncMessages + ): Promise { + const userMsg2 = payload.p2pMessages.find((m) => m.from === 0 && m.to === 2); + assert(userMsg2, 'User to Bitgo message 2 not found in P2P messages'); + assert(userMsg2.commitment, 'User to Bitgo commitment not found in P2P messages'); + const backupMsg2 = payload.p2pMessages.find((m) => m.from === 1 && m.to === 2); + assert(backupMsg2, 'Backup to Bitgo message 2 not found in P2P messages'); + assert(backupMsg2.commitment, 'Backup to Bitgo commitment not found in P2P messages'); + + return this.sendKeyGenerationRequest(enterprise, KeyGenStateRoundEnum['MPCv2-R2'], { + sessionId, + userMsg2: { + from: 0, + to: 2, + signature: userMsg2.payload.signature, + encryptedMessage: userMsg2.payload.encryptedMessage, + }, + userCommitment2: userMsg2.commitment, + backupMsg2: { + from: 1, + to: 2, + signature: backupMsg2.payload.signature, + encryptedMessage: backupMsg2.payload.encryptedMessage, + }, + backupCommitment2: backupMsg2.commitment, + }); + } + + private async sendKeyGenerationRound3( + enterprise: string, + sessionId: string, + payload: DklsTypes.AuthEncMessages + ): Promise { + const userMsg3 = payload.p2pMessages.find((m) => m.from === 0 && m.to === 2)?.payload; + assert(userMsg3, 'User to Bitgo message 3 not found in P2P messages'); + const backupMsg3 = payload.p2pMessages.find((m) => m.from === 1 && m.to === 2)?.payload; + assert(backupMsg3, 'Backup to Bitgo message 3 not found in P2P messages'); + const userMsg4 = payload.broadcastMessages.find((m) => m.from === 0)?.payload; + assert(userMsg4, 'User message 1 not found in broadcast messages'); + const backupMsg4 = payload.broadcastMessages.find((m) => m.from === 1)?.payload; + assert(backupMsg4, 'Backup message 1 not found in broadcast messages'); + + return this.sendKeyGenerationRequest(enterprise, KeyGenStateRoundEnum['MPCv2-R3'], { + sessionId, + userMsg3: { from: 0, to: 2, ...userMsg3 }, + backupMsg3: { from: 1, to: 2, ...backupMsg3 }, + userMsg4: { from: 0, ...userMsg4 }, + backupMsg4: { from: 1, ...backupMsg4 }, + }); + } + + // #endregion + + // #region utils + private formatBitgoBroadcastMessage(broadcastMessage: MPCv2BroadcastMessage) { + return { + from: broadcastMessage.from, + payload: { message: broadcastMessage.message, signature: broadcastMessage.signature }, + }; + } + + private formatP2PMessage(p2pMessage: MPCv2P2PMessage, commitment?: string) { + return { + payload: { encryptedMessage: p2pMessage.encryptedMessage, signature: p2pMessage.signature }, + from: p2pMessage.from, + to: p2pMessage.to, + commitment, + }; + } + // #endregion +} diff --git a/modules/sdk-core/src/bitgo/utils/tss/ecdsa/index.ts b/modules/sdk-core/src/bitgo/utils/tss/ecdsa/index.ts index 55cee34566..05f40e526e 100644 --- a/modules/sdk-core/src/bitgo/utils/tss/ecdsa/index.ts +++ b/modules/sdk-core/src/bitgo/utils/tss/ecdsa/index.ts @@ -1,2 +1,4 @@ export * from './ecdsa'; +export * from './ecdsaMPCv2'; export * from './types'; +export * from './typesMPCv2'; diff --git a/modules/sdk-core/src/bitgo/utils/tss/ecdsa/typesMPCv2.ts b/modules/sdk-core/src/bitgo/utils/tss/ecdsa/typesMPCv2.ts new file mode 100644 index 0000000000..4f80e72f79 --- /dev/null +++ b/modules/sdk-core/src/bitgo/utils/tss/ecdsa/typesMPCv2.ts @@ -0,0 +1,117 @@ +import * as t from 'io-ts'; + +export enum MPCv2IndexesEnum { + USER = 0, + BACKUP = 1, + BITGO = 2, +} +export const mpcv2Indexes = t.keyof(MPCv2IndexesEnum); +export type MPCv2Indexes = t.TypeOf; + +export const mpcv2BroadcastMessage = t.type({ + from: t.union([t.literal(0), t.literal(1), t.literal(2)]), + message: t.string, + signature: t.string, +}); + +export type MPCv2BroadcastMessage = t.TypeOf; + +export const mpcv2P2PMessage = t.type({ + from: t.union([t.literal(0), t.literal(1), t.literal(2)]), + to: t.union([t.literal(0), t.literal(1), t.literal(2)]), + encryptedMessage: t.string, + signature: t.string, +}); + +export type MPCv2P2PMessage = t.TypeOf; + +export enum MPCv2StateRoundEnum { + 'MPCv2-R1' = 'MPCv2-R1', + 'MPCv2-R2' = 'MPCv2-R2', + 'MPCv2-R3' = 'MPCv2-R3', +} +export const mpcv2StateRound = t.keyof(MPCv2StateRoundEnum); +export type MPCv2StateRound = t.TypeOf; + +export const KeyGenStateRoundEnum = { + ...MPCv2StateRoundEnum, +}; +export const keyGenStateRound = t.keyof(KeyGenStateRoundEnum); +export type KeyGenStateRound = t.TypeOf; + +export const mpcv2KeyGenRound1Request = t.type({ + userGpgPublicKey: t.string, + backupGpgPublicKey: t.string, + userMsg1: mpcv2BroadcastMessage, + backupMsg1: mpcv2BroadcastMessage, +}); + +export type MPCv2KeyGenRound1Request = t.TypeOf; + +export const mpcv2KeyGenRound1Response = t.type({ + sessionId: t.string, + bitgoMsg1: mpcv2BroadcastMessage, + bitgoToUserMsg2: mpcv2P2PMessage, + bitgoToBackupMsg2: mpcv2P2PMessage, +}); + +export type MPCv2KeyGenRound1Response = t.TypeOf; + +export const mpcv2KeyGenRound2Request = t.type({ + sessionId: t.string, + userMsg2: mpcv2P2PMessage, + userCommitment2: t.string, + backupMsg2: mpcv2P2PMessage, + backupCommitment2: t.string, +}); + +export type MPCv2KeyGenRound2Request = t.TypeOf; + +export const mpcv2KeyGenRound2Response = t.type({ + sessionId: t.string, + bitgoCommitment2: t.string, + bitgoToUserMsg3: mpcv2P2PMessage, + bitgoToBackupMsg3: mpcv2P2PMessage, +}); + +export type MPCv2KeyGenRound2Response = t.TypeOf; + +export const mpcv2KeyGenRound3Request = t.type({ + sessionId: t.string, + userMsg3: mpcv2P2PMessage, + backupMsg3: mpcv2P2PMessage, + userMsg4: mpcv2BroadcastMessage, + backupMsg4: mpcv2BroadcastMessage, +}); + +export type MPCv2KeyGenRound3Request = t.TypeOf; + +export const mpcv2KeyGenRound3Response = t.type({ + sessionId: t.string, + commonKeychain: t.string, + bitgoMsg4: mpcv2BroadcastMessage, +}); + +export type MPCv2KeyGenRound3Response = t.TypeOf; + +export const generateMPCv2KeyRequestBody = t.union([ + mpcv2KeyGenRound1Request, + mpcv2KeyGenRound2Request, + mpcv2KeyGenRound3Request, +]); + +export type GenerateMPCv2KeyRequestBody = t.TypeOf; + +export const generateMPCv2KeyRequestResponse = t.union([ + mpcv2KeyGenRound1Response, + mpcv2KeyGenRound2Response, + mpcv2KeyGenRound3Response, +]); + +export type GenerateMPCv2KeyRequestResponse = t.TypeOf; + +export enum KeyGenTypeEnum { + MPCv2 = 'MPCv2', +} +export const keyGenType = t.keyof(KeyGenTypeEnum); +export type KeyGenType = t.TypeOf; diff --git a/modules/sdk-core/src/bitgo/wallet/wallets.ts b/modules/sdk-core/src/bitgo/wallet/wallets.ts index e4b67ec5f1..04d5c14204 100644 --- a/modules/sdk-core/src/bitgo/wallet/wallets.ts +++ b/modules/sdk-core/src/bitgo/wallet/wallets.ts @@ -216,18 +216,24 @@ export class Wallets implements IWallets { walletParams.enterprise = enterprise; } - // EVM TSS wallets must use wallet version 3 - if ((isTss && this.baseCoin.isEVM()) !== (params.walletVersion === 3)) { - throw new Error('EVM TSS wallets are only supported for wallet version 3'); + // EVM TSS wallets must use wallet version 3 and 5 + if ((isTss && this.baseCoin.isEVM()) !== (params.walletVersion === 3 || params.walletVersion === 5)) { + throw new Error('EVM TSS wallets are only supported for wallet version 3 and 5'); } if (isTss) { if (!this.baseCoin.supportsTss()) { throw new Error(`coin ${this.baseCoin.getFamily()} does not support TSS at this time`); } + if (params.walletVersion === 5 && !this.baseCoin.supportsMPCv2()) { + throw new Error(`coin ${this.baseCoin.getFamily()} does not support TSS MPCv2 at this time`); + } assert(enterprise, 'enterprise is required for TSS wallet'); if (type === 'cold') { + if (params.walletVersion === 5) { + throw new Error('EVM TSS MPCv2 wallets are not supported for cold wallets'); + } // validate assert(params.bitgoKeyId, 'bitgoKeyId is required for SMC TSS wallet'); assert(params.commonKeychain, 'commonKeychain is required for SMC TSS wallet'); @@ -243,6 +249,9 @@ export class Wallets implements IWallets { } if (type === 'custodial') { + if (params.walletVersion === 5) { + throw new Error('EVM TSS MPCv2 wallets are not supported for custodial wallets'); + } return this.generateCustodialMpcWallet({ multisigType: 'tss', label, @@ -252,6 +261,7 @@ export class Wallets implements IWallets { } assert(passphrase, 'cannot generate TSS keys without passphrase'); + return this.generateMpcWallet({ multisigType: 'tss', label, @@ -685,6 +695,7 @@ export class Wallets implements IWallets { enterprise, originalPasscodeEncryptionCode, backupProvider, + walletVersion, }); // Create Wallet diff --git a/modules/sdk-lib-mpc/src/tss/ecdsa-dkls/dkg.ts b/modules/sdk-lib-mpc/src/tss/ecdsa-dkls/dkg.ts index 807d937177..6366696579 100644 --- a/modules/sdk-lib-mpc/src/tss/ecdsa-dkls/dkg.ts +++ b/modules/sdk-lib-mpc/src/tss/ecdsa-dkls/dkg.ts @@ -90,7 +90,7 @@ export class Dkg { throw Error('Session not initialized'); } try { - if (this.dkgState == DkgState.Round3) { + if (this.dkgState === DkgState.Round3) { const commitmentsUnsorted = messagesForIthRound.p2pMessages .map((m) => { return { from: m.from, commitment: m.commitment }; @@ -115,15 +115,15 @@ export class Dkg { undefined ); } - if (this.dkgState == DkgState.Round4) { + if (this.dkgState === DkgState.Round4) { this.dkgKeyShare = this.dkgSession.keyshare(); this.dkgState = DkgState.Complete; return { broadcastMessages: [], p2pMessages: [] }; } else { - // Update ronud data. + // Update round data. this._deserializeState(); } - if (this.dkgState == DkgState.Round2) { + if (this.dkgState === DkgState.Round2) { this.chainCodeCommitment = this.dkgSession.calculateChainCodeCommitment(); } nextRoundDeserializedMessages = {