diff --git a/modules/bitgo/package.json b/modules/bitgo/package.json index 1818ef38a1..eb4234d34d 100644 --- a/modules/bitgo/package.json +++ b/modules/bitgo/package.json @@ -114,6 +114,7 @@ }, "devDependencies": { "@bitgo/sdk-test": "^8.0.11", + "@bitgo/public-types": "2.7.0", "@openpgp/web-stream-tools": "0.0.14", "@types/create-hmac": "^1.1.0", "@types/debug": "^4.1.4", 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..3d483f7a4b --- /dev/null +++ b/modules/bitgo/test/v2/unit/internal/tssUtils/ecdsaMPCv2.ts @@ -0,0 +1,429 @@ +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 { + MPCv2KeyGenRound1Request, + MPCv2KeyGenRound1Response, + MPCv2KeyGenRound2Request, + MPCv2KeyGenRound2Response, + MPCv2KeyGenRound3Request, + MPCv2KeyGenRound3Response, +} from '@bitgo/public-types'; +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, + bitgoMPCv2PublicKey: 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.MPCv2PartiesEnum.BITGO, 'test'); + const usersKeychainPromise = tssUtils.createParticipantKeychain( + ECDSAUtils.MPCv2PartiesEnum.USER, + 'test', + Buffer.from('test'), + 'passphrase', + 'test' + ); + const backupKeychainPromise = tssUtils.createParticipantKeychain( + ECDSAUtils.MPCv2PartiesEnum.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, + mpcv2PublicKey: 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: 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' as any, // NonEmptyString, + 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: 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 as any, // NonEmptyString, + 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: 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: commonKeychain as any, // NonEmptyString + 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-core/package.json b/modules/sdk-core/package.json index 5629e23e18..f9cb8d2616 100644 --- a/modules/sdk-core/package.json +++ b/modules/sdk-core/package.json @@ -38,7 +38,7 @@ }, "dependencies": { "@bitgo/bls-dkg": "^1.3.1", - "@bitgo/public-types": "2.1.0", + "@bitgo/public-types": "2.7.0", "@bitgo/sdk-lib-mpc": "^9.3.0", "@bitgo/statics": "^48.7.0", "@bitgo/utxo-lib": "^9.35.0", @@ -59,6 +59,7 @@ "ethereumjs-util": "7.1.5", "fp-ts": "^2.12.2", "io-ts": "2.1.3", + "io-ts-types": "0.5.16", "keccak": "3.0.3", "libsodium-wrappers-sumo": "^0.7.9", "lodash": "^4.17.15", diff --git a/modules/sdk-core/src/bitgo/baseCoin/baseCoin.ts b/modules/sdk-core/src/bitgo/baseCoin/baseCoin.ts index c29110bbe1..77e5c7f6cf 100644 --- a/modules/sdk-core/src/bitgo/baseCoin/baseCoin.ts +++ b/modules/sdk-core/src/bitgo/baseCoin/baseCoin.ts @@ -6,6 +6,7 @@ import { bip32 } from '@bitgo/utxo-lib'; import { BigNumber } from 'bignumber.js'; import * as utxolib from '@bitgo/utxo-lib'; +import { BaseCoin as StaticsBaseCoin } from '@bitgo/statics'; import { InitiateRecoveryOptions } from '../recovery'; import { signMessage } from '../bip32util'; @@ -55,6 +56,7 @@ export abstract class BaseCoin implements IBaseCoin { protected readonly _pendingApprovals: PendingApprovals; protected readonly _markets: Markets; protected static readonly _coinTokenPatternSeparator = ':'; + protected readonly _staticsCoin: Readonly; protected constructor(bitgo: BitGoBase) { this.bitgo = bitgo; @@ -103,6 +105,14 @@ export abstract class BaseCoin implements IBaseCoin { return this.getChain(); } + /** + * Gets the statics coin object + * @returns {Readonly} the statics coin object + */ + getConfig(): Readonly { + return this._staticsCoin; + } + /** * Name of the chain which supports this coin (eg, 'btc', 'eth') */ diff --git a/modules/sdk-core/src/bitgo/baseCoin/iBaseCoin.ts b/modules/sdk-core/src/bitgo/baseCoin/iBaseCoin.ts index c733057063..be1b7e11c9 100644 --- a/modules/sdk-core/src/bitgo/baseCoin/iBaseCoin.ts +++ b/modules/sdk-core/src/bitgo/baseCoin/iBaseCoin.ts @@ -1,4 +1,5 @@ import BigNumber from 'bignumber.js'; +import { BaseCoin as StaticsBaseCoin, BaseTokenConfig } from '@bitgo/statics'; import { IRequestTracer } from '../../api'; import { IEnterprises } from '../enterprise'; import { Keychain, IKeychains } from '../keychain'; @@ -10,7 +11,6 @@ import EddsaUtils, { TxRequest } from '../utils/tss/eddsa'; import { CustomSigningFunction, IWallet, IWallets, Wallet, WalletData } from '../wallet'; import { IWebhooks } from '../webhook/iWebhooks'; -import { BaseTokenConfig } from '@bitgo/statics'; import { TransactionType } from '../../account-lib'; import { IInscriptionBuilder } from '../inscriptionBuilder'; import { Hash } from 'crypto'; @@ -448,6 +448,7 @@ export interface BaseBroadcastTransactionResult { export interface IBaseCoin { type: string; tokenConfig?: BaseTokenConfig; + getConfig(): Readonly; url(suffix: string): string; wallets(): IWallets; enterprises(): IEnterprises; 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/opengpgUtils.ts b/modules/sdk-core/src/bitgo/utils/opengpgUtils.ts index ec5b8f1cdf..d6be0da7d0 100644 --- a/modules/sdk-core/src/bitgo/utils/opengpgUtils.ts +++ b/modules/sdk-core/src/bitgo/utils/opengpgUtils.ts @@ -45,6 +45,21 @@ export async function getBitgoGpgPubKey(bitgo: BitGoBase): Promise { return await readKey({ armoredKey: bitgoPublicKeyStr }); } +/** + * Fetches BitGo's MPCv2 public gpg key used in MPC flows + * @param {BitGoBase} bitgo BitGo object + * @return {Key} public gpg key + */ +export async function getBitgoMPCv2GpgPubKey(bitgo: BitGoBase): Promise { + const constants = await bitgo.fetchConstants(); + if (!constants.mpc || !constants.mpc.bitgoMPCv2PublicKey) { + throw new Error('Unable to create MPC keys - bitgoPublicKey is missing from constants'); + } + + const bitgoMPCv2PublicKeyStr = constants.mpc.bitgoMPCv2PublicKey as string; + return await readKey({ armoredKey: bitgoMPCv2PublicKeyStr }); +} + /** * Verifies the primary user on a GPG key using a reference key representing the user to be checked. * Allows a verification without a date check by wrapping verifyPrimaryUser of openpgp. diff --git a/modules/sdk-core/src/bitgo/utils/tss/baseTSSUtils.ts b/modules/sdk-core/src/bitgo/utils/tss/baseTSSUtils.ts index c068c15499..c86d384f34 100644 --- a/modules/sdk-core/src/bitgo/utils/tss/baseTSSUtils.ts +++ b/modules/sdk-core/src/bitgo/utils/tss/baseTSSUtils.ts @@ -387,14 +387,15 @@ export default class BaseTssUtils extends MpcUtils implements ITssUtil * It gets the appropriate BitGo GPG public key for key creation based on a * combination of coin and the feature flags on the user and their enterprise if set. * @param enterpriseId - enterprise under which user wants to create the wallet + * @param isMPCv2 - true to get the MPCv2 GPG public key, defaults to false */ - public async getBitgoGpgPubkeyBasedOnFeatureFlags(enterpriseId: string | undefined): Promise { + public async getBitgoGpgPubkeyBasedOnFeatureFlags(enterpriseId: string | undefined, isMPCv2 = false): Promise { const response: BitgoGPGPublicKey = await this.bitgo .get(this.baseCoin.url('/tss/pubkey')) .query({ enterpriseId }) .result(); - const bitgoPublicKeyStr = response.publicKey as string; - return readKey({ armoredKey: bitgoPublicKeyStr }); + const bitgoPublicKeyStr = isMPCv2 ? response.mpcv2PublicKey : response.publicKey; + return readKey({ armoredKey: bitgoPublicKeyStr as string }); } /** diff --git a/modules/sdk-core/src/bitgo/utils/tss/baseTypes.ts b/modules/sdk-core/src/bitgo/utils/tss/baseTypes.ts index 4c06df9b84..afcc7e9a0e 100644 --- a/modules/sdk-core/src/bitgo/utils/tss/baseTypes.ts +++ b/modules/sdk-core/src/bitgo/utils/tss/baseTypes.ts @@ -367,6 +367,7 @@ export interface BackupKeyShare { export interface BitgoGPGPublicKey { name: string; publicKey: string; + mpcv2PublicKey?: string; enterpriseId: string; } 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..3b2ac3e84d --- /dev/null +++ b/modules/sdk-core/src/bitgo/utils/tss/ecdsa/base.ts @@ -0,0 +1,75 @@ +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, getBitgoMPCv2GpgPubKey, 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; + protected bitgoMPCv2PublicGpgKey: 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); + this.bitgoMPCv2PublicGpgKey = await getBitgoMPCv2GpgPubKey(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..5fd87cdac4 --- /dev/null +++ b/modules/sdk-core/src/bitgo/utils/tss/ecdsa/ecdsaMPCv2.ts @@ -0,0 +1,495 @@ +import assert from 'assert'; +import { NonEmptyString } from 'io-ts-types'; +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 { + MPCv2Party, + MPCv2KeyGenState, + MPCv2BroadcastMessage, + MPCv2KeyGenRound1Response, + MPCv2KeyGenRound2Response, + MPCv2KeyGenRound3Response, + MPCv2P2PMessage, + KeyGenTypeEnum, + MPCv2KeyGenStateEnum, +} from '@bitgo/public-types'; +import { GenerateMPCv2KeyRequestBody, GenerateMPCv2KeyRequestResponse, MPCv2PartiesEnum } 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, MPCv2PartiesEnum.USER); + const backupSession = new DklsDkg.Dkg(n, m, MPCv2PartiesEnum.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, true)) ?? this.bitgoPublicGpgKey + ).armor(); + + const userGpgPrvKey: DklsTypes.PartyGpgKey = { + partyId: MPCv2PartiesEnum.USER, + gpgKey: userGpgKey.privateKey, + }; + const backupGpgPrvKey: DklsTypes.PartyGpgKey = { + partyId: MPCv2PartiesEnum.BACKUP, + gpgKey: backupGpgKey.privateKey, + }; + const bitgoGpgPubKey: DklsTypes.PartyGpgKey = { + partyId: MPCv2PartiesEnum.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 === MPCv2PartiesEnum.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 === MPCv2PartiesEnum.USER && m.to === MPCv2PartiesEnum.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 === MPCv2PartiesEnum.BACKUP && m.to === MPCv2PartiesEnum.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 === MPCv2PartiesEnum.BITGO && m.to === MPCv2PartiesEnum.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 === MPCv2PartiesEnum.BITGO && m.to === MPCv2PartiesEnum.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 === MPCv2PartiesEnum.USER && m.to === MPCv2PartiesEnum.BACKUP + ); + assert(userToBackupMsg2, 'User to Backup message 2 not found in P2P messages'); + + const backupToUserMsg2 = backupRound2P2PMessages.p2pMessages.find( + (m) => m.from === MPCv2PartiesEnum.BACKUP && m.to === MPCv2PartiesEnum.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 === MPCv2PartiesEnum.USER && m.to === MPCv2PartiesEnum.BACKUP + ); + assert(userToBackupMsg3, 'User to Backup message 3 not found in P2P messages'); + const userToBitgoMsg3 = userRound3Messages.p2pMessages.find( + (m) => m.from === MPCv2PartiesEnum.USER && m.to === MPCv2PartiesEnum.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 === MPCv2PartiesEnum.BACKUP && m.to === MPCv2PartiesEnum.USER + ); + assert(backupToUserMsg3, 'Backup to User message 3 not found in P2P messages'); + const backupToBitgoMsg3 = backupRound3Messages.p2pMessages.find( + (m) => m.from === MPCv2PartiesEnum.BACKUP && m.to === MPCv2PartiesEnum.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 === MPCv2PartiesEnum.BITGO && m.to === MPCv2PartiesEnum.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 === MPCv2PartiesEnum.BITGO && m.to === MPCv2PartiesEnum.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 === MPCv2PartiesEnum.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 === MPCv2PartiesEnum.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 === MPCv2PartiesEnum.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: MPCv2Party, + commonKeychain: string, + privateMaterial?: Buffer, + passphrase?: string, + originalPasscodeEncryptionCode?: string + ): Promise { + let source: string; + let encryptedPrv: string | undefined = undefined; + switch (participantIndex) { + case MPCv2PartiesEnum.USER: + case MPCv2PartiesEnum.BACKUP: + source = participantIndex === MPCv2PartiesEnum.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 MPCv2PartiesEnum.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( + MPCv2PartiesEnum.USER, + commonKeychain, + privateMaterial, + passphrase, + originalPasscodeEncryptionCode + ); + } + + private async addBackupKeychain( + commonKeychain: string, + privateMaterial: Buffer, + passphrase: string, + originalPasscodeEncryptionCode?: string + ): Promise { + return this.createParticipantKeychain( + MPCv2PartiesEnum.BACKUP, + commonKeychain, + privateMaterial, + passphrase, + originalPasscodeEncryptionCode + ); + } + + private async addBitgoKeychain(commonKeychain: string): Promise { + return this.createParticipantKeychain(MPCv2PartiesEnum.BITGO, commonKeychain); + } + // #endregion + + // #region generate key request utils + private async sendKeyGenerationRequest( + enterprise: string, + round: MPCv2KeyGenState, + 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 { + assert(NonEmptyString.is(userGpgPublicKey), 'User GPG public key is required'); + assert(NonEmptyString.is(backupGpgPublicKey), 'Backup GPG public key is required'); + const userMsg1 = payload.broadcastMessages.find((m) => m.from === MPCv2PartiesEnum.USER)?.payload; + assert(userMsg1, 'User message 1 not found in broadcast messages'); + const backupMsg1 = payload.broadcastMessages.find((m) => m.from === MPCv2PartiesEnum.BACKUP)?.payload; + assert(backupMsg1, 'Backup message 1 not found in broadcast messages'); + + return this.sendKeyGenerationRequest(enterprise, MPCv2KeyGenStateEnum['MPCv2-R1'], { + userGpgPublicKey, + backupGpgPublicKey, + userMsg1: { from: 0, ...userMsg1 }, + backupMsg1: { from: 1, ...backupMsg1 }, + }); + } + + private async sendKeyGenerationRound2( + enterprise: string, + sessionId: string, + payload: DklsTypes.AuthEncMessages + ): Promise { + assert(NonEmptyString.is(sessionId), 'Session ID is required'); + const userMsg2 = payload.p2pMessages.find( + (m) => m.from === MPCv2PartiesEnum.USER && m.to === MPCv2PartiesEnum.BITGO + ); + assert(userMsg2, 'User to Bitgo message 2 not found in P2P messages'); + assert(userMsg2.commitment, 'User to Bitgo commitment not found in P2P messages'); + assert(NonEmptyString.is(userMsg2.commitment), 'User to Bitgo commitment is required'); + const backupMsg2 = payload.p2pMessages.find( + (m) => m.from === MPCv2PartiesEnum.BACKUP && m.to === MPCv2PartiesEnum.BITGO + ); + assert(backupMsg2, 'Backup to Bitgo message 2 not found in P2P messages'); + assert(backupMsg2.commitment, 'Backup to Bitgo commitment not found in P2P messages'); + assert(NonEmptyString.is(backupMsg2.commitment), 'Backup to Bitgo commitment is required'); + + return this.sendKeyGenerationRequest(enterprise, MPCv2KeyGenStateEnum['MPCv2-R2'], { + sessionId, + userMsg2: { + from: MPCv2PartiesEnum.USER, + to: MPCv2PartiesEnum.BITGO, + signature: userMsg2.payload.signature, + encryptedMessage: userMsg2.payload.encryptedMessage, + }, + userCommitment2: userMsg2.commitment, + backupMsg2: { + from: MPCv2PartiesEnum.BACKUP, + to: MPCv2PartiesEnum.BITGO, + signature: backupMsg2.payload.signature, + encryptedMessage: backupMsg2.payload.encryptedMessage, + }, + backupCommitment2: backupMsg2.commitment, + }); + } + + private async sendKeyGenerationRound3( + enterprise: string, + sessionId: string, + payload: DklsTypes.AuthEncMessages + ): Promise { + assert(NonEmptyString.is(sessionId), 'Session ID is required'); + const userMsg3 = payload.p2pMessages.find( + (m) => m.from === MPCv2PartiesEnum.USER && m.to === MPCv2PartiesEnum.BITGO + )?.payload; + assert(userMsg3, 'User to Bitgo message 3 not found in P2P messages'); + const backupMsg3 = payload.p2pMessages.find( + (m) => m.from === MPCv2PartiesEnum.BACKUP && m.to === MPCv2PartiesEnum.BITGO + )?.payload; + assert(backupMsg3, 'Backup to Bitgo message 3 not found in P2P messages'); + const userMsg4 = payload.broadcastMessages.find((m) => m.from === MPCv2PartiesEnum.USER)?.payload; + assert(userMsg4, 'User message 1 not found in broadcast messages'); + const backupMsg4 = payload.broadcastMessages.find((m) => m.from === MPCv2PartiesEnum.BACKUP)?.payload; + assert(backupMsg4, 'Backup message 1 not found in broadcast messages'); + + return this.sendKeyGenerationRequest(enterprise, MPCv2KeyGenStateEnum['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..399ece951b --- /dev/null +++ b/modules/sdk-core/src/bitgo/utils/tss/ecdsa/typesMPCv2.ts @@ -0,0 +1,31 @@ +import * as t from 'io-ts'; +import { + MPCv2KeyGenRound1Request, + MPCv2KeyGenRound1Response, + MPCv2KeyGenRound2Request, + MPCv2KeyGenRound2Response, + MPCv2KeyGenRound3Request, + MPCv2KeyGenRound3Response, +} from '@bitgo/public-types'; + +export enum MPCv2PartiesEnum { + USER = 0, + BACKUP = 1, + BITGO = 2, +} + +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; diff --git a/modules/sdk-core/src/bitgo/wallet/wallets.ts b/modules/sdk-core/src/bitgo/wallet/wallets.ts index e4b67ec5f1..9c29fa4480 100644 --- a/modules/sdk-core/src/bitgo/wallet/wallets.ts +++ b/modules/sdk-core/src/bitgo/wallet/wallets.ts @@ -5,6 +5,8 @@ import assert from 'assert'; import { BigNumber } from 'bignumber.js'; import { bip32 } from '@bitgo/utxo-lib'; import * as _ from 'lodash'; +import { CoinFeature } from '@bitgo/statics'; + import { sanitizeLegacyPath } from '../../api'; import * as common from '../../common'; import { IBaseCoin, KeychainsTriplet, SupplementGenerateWalletOptions } from '../baseCoin'; @@ -216,18 +218,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.getConfig().features.includes(CoinFeature.MPCV2)) { + 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 +251,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 +263,7 @@ export class Wallets implements IWallets { } assert(passphrase, 'cannot generate TSS keys without passphrase'); + return this.generateMpcWallet({ multisigType: 'tss', label, @@ -685,6 +697,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 = { diff --git a/modules/statics/src/base.ts b/modules/statics/src/base.ts index 4a820b6c47..5f02d8b2a4 100644 --- a/modules/statics/src/base.ts +++ b/modules/statics/src/base.ts @@ -260,6 +260,11 @@ export enum CoinFeature { * This coin uses non-packed encoding for transaction data */ USES_NON_PACKED_ENCODING_FOR_TXDATA = 'uses-non-packed-encoding-for-txdata', + + /** + * This coins supports MPCv2 for key creation and signing + */ + MPCV2 = 'mpcv2', } /** diff --git a/modules/statics/src/coins.ts b/modules/statics/src/coins.ts index c2dc924b47..2651985b15 100644 --- a/modules/statics/src/coins.ts +++ b/modules/statics/src/coins.ts @@ -554,6 +554,7 @@ export const coins = CoinMap.fromCoins([ ...ETH_FEATURES_WITH_STAKING_AND_MMI, CoinFeature.TSS, CoinFeature.TSS_COLD, + CoinFeature.MPCV2, CoinFeature.MULTISIG_COLD, CoinFeature.EVM_WALLET, CoinFeature.CUSTODY_BITGO_GERMANY, @@ -607,6 +608,7 @@ export const coins = CoinMap.fromCoins([ ...ETH_FEATURES_WITH_STAKING_AND_MMI, CoinFeature.TSS, CoinFeature.TSS_COLD, + CoinFeature.MPCV2, CoinFeature.MULTISIG_COLD, CoinFeature.EVM_WALLET, CoinFeature.CUSTODY_BITGO_GERMANY, diff --git a/yarn.lock b/yarn.lock index ed87598d53..f3b28f673f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1056,10 +1056,10 @@ "@scure/base" "1.1.5" micro-eth-signer "0.7.2" -"@bitgo/public-types@2.1.0": - version "2.1.0" - resolved "https://registry.yarnpkg.com/@bitgo/public-types/-/public-types-2.1.0.tgz#b9ede95d490907f610cdc5e9ec4156e13645b852" - integrity sha512-EvjvArAExJd/tY8PPii3SE3lWOpAJ2+lJBtn3DVMmTROXfS7q9nLvBbAZHPZZ3Q/VkPEeAfJ7KZ2Fvbr/TgtQg== +"@bitgo/public-types@2.7.0": + version "2.7.0" + resolved "https://registry.yarnpkg.com/@bitgo/public-types/-/public-types-2.7.0.tgz#f95640502d18a2c767a9c3975fa666304c479780" + integrity sha512-U8A3CESZCkz+gO5cEiaiPqZwsag0Dav7WygP07dXgYUZSYNcM32DPoAllyQUneFlyZ4BZ2xnNIuFci4R6XTLYA== dependencies: "@api-ts/io-ts-http" "1.0.0" fp-ts "2.16.2"