Skip to content

Commit

Permalink
feat(sdk-core): add MPCv2 wallet creation
Browse files Browse the repository at this point in the history
added MPCv2 wallet creation

WP-1686

TICKET: WP-1686
  • Loading branch information
alebusse committed Apr 16, 2024
1 parent a8528d3 commit fb2416d
Show file tree
Hide file tree
Showing 14 changed files with 1,256 additions and 80 deletions.
428 changes: 428 additions & 0 deletions modules/bitgo/test/v2/unit/internal/tssUtils/ecdsaMPCv2.ts

Large diffs are not rendered by default.

109 changes: 104 additions & 5 deletions modules/bitgo/test/v2/unit/wallets.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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' });
Expand Down Expand Up @@ -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');

Expand Down
5 changes: 5 additions & 0 deletions modules/sdk-coin-eth/src/eth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,11 @@ export class Eth extends AbstractEthLikeNewCoins {
return true;
}

/** @inheritDoc */
supportsMPCv2(): boolean {
return true;
}

getMPCAlgorithm(): MPCAlgorithm {
return 'ecdsa';
}
Expand Down
8 changes: 8 additions & 0 deletions modules/sdk-core/src/bitgo/baseCoin/baseCoin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
1 change: 1 addition & 0 deletions modules/sdk-core/src/bitgo/baseCoin/iBaseCoin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -464,6 +464,7 @@ export interface IBaseCoin {
allowsAccountConsolidations(): boolean;
getTokenEnablementConfig(): TokenEnablementConfig;
supportsTss(): boolean;
supportsMPCv2(): boolean;
supportsDeriveKeyWithSeed(): boolean;
isEVM(): boolean;
supportsBlsDkg(): boolean;
Expand Down
1 change: 1 addition & 0 deletions modules/sdk-core/src/bitgo/keychain/iKeychains.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ export interface CreateMpcOptions {
originalPasscodeEncryptionCode?: string;
enterprise?: string;
backupProvider?: BackupProvider;
walletVersion?: number;
}

export interface GetKeysForSigningOptions {
Expand Down
7 changes: 6 additions & 1 deletion modules/sdk-core/src/bitgo/keychain/keychains.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
: params.walletVersion === 5
? ECDSAUtils.EcdsaMPCv2Utils
: ECDSAUtils.EcdsaUtils;
break;
case 'blsdkg':
if (_.isUndefined(params.passphrase)) {
Expand Down
73 changes: 73 additions & 0 deletions modules/sdk-core/src/bitgo/utils/tss/ecdsa/base.ts
Original file line number Diff line number Diff line change
@@ -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<KeyShare> {
// 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<openpgp.Key> {
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<BackupGpgKey> {
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;
}
}
72 changes: 5 additions & 67 deletions modules/sdk-core/src/bitgo/utils/tss/ecdsa/ecdsa.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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,
Expand All @@ -22,7 +20,6 @@ import {
KeyShare,
} from './types';
import {
BackupGpgKey,
BackupKeyShare,
BitgoHeldBackupKeyShare,
CustomKShareGeneratingFunction,
Expand All @@ -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';
Expand All @@ -48,49 +45,12 @@ import {
TssEcdsaStep2ReturnMessage,
TxRequestChallengeResponse,
} from '../../../tss/types';
import { BaseEcdsaUtils } from './base';

const encryptNShare = ECDSAMethods.encryptNShare;

/** @inheritdoc */
export class EcdsaUtils extends baseTSSUtils<KeyShare> {
// 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<openpgp.Key> {
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,
Expand Down Expand Up @@ -228,15 +188,6 @@ export class EcdsaUtils extends baseTSSUtils<KeyShare> {
return backupKeyShare;
}

/**
* Gets backup pub gpg key string
* if a third party provided then get from trust
* @param isThirdPartyBackup
*/
async getBackupGpgPubKey(isThirdPartyBackup = false): Promise<BackupGpgKey> {
return isThirdPartyBackup ? getTrustGpgPubKey(this.bitgo) : generateGPGKeyPair('secp256k1');
}

createUserKeychain({
userGpgKey,
backupGpgKey,
Expand Down Expand Up @@ -1409,17 +1360,4 @@ export class EcdsaUtils extends baseTSSUtils<KeyShare> {
.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);
}
}
Loading

0 comments on commit fb2416d

Please sign in to comment.