UNPKG

bitgo

Version:
885 lines • 252 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const assert = require("assert"); const _ = require("lodash"); const nock = require("nock"); const openpgp = require("openpgp"); const should = require("should"); const sinon = require("sinon"); const sdk_test_1 = require("@bitgo/sdk-test"); const src_1 = require("../../../../../src"); const sdk_core_1 = require("@bitgo/sdk-core"); const sdk_lib_mpc_1 = require("@bitgo/sdk-lib-mpc"); const ecdsaFixtures_1 = require("../../../fixtures/tss/ecdsaFixtures"); const common_1 = require("./common"); const helpers_1 = require("../../tss/helpers"); const utxo_lib_1 = require("@bitgo/utxo-lib"); const ecdsaNtilde_1 = require("./mocks/ecdsaNtilde"); const sdk_opensslbytes_1 = require("@bitgo/sdk-opensslbytes"); const openSSLBytes = (0, sdk_opensslbytes_1.loadWebAssembly)().buffer; const createKeccakHash = require('keccak'); const encryptNShare = sdk_core_1.ECDSAMethods.encryptNShare; openpgp.config.rejectCurves = new Set(); describe('TSS Ecdsa Utils:', async function () { const coinName = 'hteth'; const reqId = new sdk_core_1.RequestTracer(); const walletId = '5b34252f1bf349930e34020a00000000'; const enterpriseId = '6449153a6f6bc20006d66771cdbe15d3'; const enterpriseData = { id: enterpriseId, name: 'Test Enterprise' }; let sandbox; let MPC; let bgUrl; let tssUtils; let wallet; let bitgo; let baseCoin; let bitgoKeyShare; let userKeyShare; let backupKeyShare; let bitgoPublicKey; let userGpgKey; let userLocalBackupGpgKey; let bitGoGPGKeyPair; let nockedBitGoKeychain; let nockedUserKeychain; beforeEach(async function () { sandbox = sinon.createSandbox(); }); afterEach(function () { sandbox.restore(); }); before(async function () { nock.cleanAll(); MPC = new sdk_core_1.Ecdsa(); userKeyShare = ecdsaFixtures_1.keyShares.userKeyShare; backupKeyShare = ecdsaFixtures_1.keyShares.backupKeyShare; bitgoKeyShare = ecdsaFixtures_1.keyShares.bitgoKeyShare; const gpgKeyPromises = [ openpgp.generateKey({ userIDs: [ { name: 'test', email: 'test@test.com', }, ], curve: 'secp256k1', }), openpgp.generateKey({ userIDs: [ { name: 'backup', email: 'backup@test.com', }, ], curve: 'secp256k1', }), openpgp.generateKey({ userIDs: [ { name: 'bitgo', email: 'bitgo@test.com', }, ], curve: 'secp256k1', }), ]; [userGpgKey, userLocalBackupGpgKey, bitGoGPGKeyPair] = await Promise.all(gpgKeyPromises); bitgoPublicKey = await openpgp.readKey({ armoredKey: bitGoGPGKeyPair.publicKey }); const constants = { mpc: { bitgoPublicKey: bitGoGPGKeyPair.publicKey, }, }; bitgo = sdk_test_1.TestBitGo.decorate(src_1.BitGo, { env: 'mock' }); bitgo.initializeTestVars(); baseCoin = bitgo.coin(coinName); bgUrl = sdk_core_1.common.Environments[bitgo.getEnv()].uri; // TODO(WP-346): sdk-test mocks conflict so we can't use persist nock(bgUrl).get('/api/v1/client/constants').times(16).reply(200, { ttl: 3600, constants }); const nockPromises = [ nockBitgoKeychain({ coin: coinName, userKeyShare, backupKeyShare, bitgoKeyShare, userGpgKey, userLocalBackupGpgKey, bitgoGpgKey: bitGoGPGKeyPair, }), nockKeychain({ coin: coinName, keyChain: { id: '1', pub: '', type: 'tss' }, source: 'user' }), nockKeychain({ coin: coinName, keyChain: { id: '2', pub: '', type: 'tss' }, source: 'backup' }), ]; [nockedBitGoKeychain, nockedUserKeychain] = await Promise.all(nockPromises); const walletData = { id: walletId, enterprise: enterpriseId, coin: coinName, coinSpecific: {}, multisigType: 'tss', }; wallet = new sdk_core_1.Wallet(bitgo, baseCoin, walletData); tssUtils = new sdk_core_1.ECDSAUtils.EcdsaUtils(bitgo, baseCoin, wallet); }); after(function () { nock.cleanAll(); }); describe('TSS key chains', async function () { it('should create backup key share held by BitGo', async function () { const enterpriseId = 'enterprise id'; const expectedKeyShare = await nockCreateBitgoHeldBackupKeyShare(coinName, enterpriseId, userGpgKey, backupKeyShare, bitGoGPGKeyPair); const result = await tssUtils.createBitgoHeldBackupKeyShare(userGpgKey, enterpriseId); result.should.eql(expectedKeyShare); }); it('should finalize backup key share held by BitGo', async function () { const commonKeychain = '4428'; const originalKeyShare = await createIncompleteBitgoHeldBackupKeyShare(userGpgKey, backupKeyShare, bitGoGPGKeyPair); const expectedFinalKeyShare = await nockFinalizeBitgoHeldBackupKeyShare(coinName, originalKeyShare, commonKeychain, userKeyShare, bitGoGPGKeyPair, nockedBitGoKeychain); const result = await tssUtils.finalizeBitgoHeldBackupKeyShare(originalKeyShare.id, commonKeychain, userKeyShare, nockedBitGoKeychain, userGpgKey, bitgoPublicKey); result.should.eql(expectedFinalKeyShare); }); it('should get the respective backup key shares based on provider', async function () { const enterpriseId = 'enterprise id'; await nockCreateBitgoHeldBackupKeyShare(coinName, enterpriseId, userGpgKey, backupKeyShare, bitGoGPGKeyPair); const backupKeyShares = await tssUtils.createBackupKeyShares(); should.exist(backupKeyShares.userHeldKeyShare); should.not.exist(backupKeyShares.bitGoHeldKeyShares); }); it('should get the correct bitgo gpg key based on coin and feature flags', async function () { const nitroGPGKeypair = await openpgp.generateKey({ userIDs: [ { name: 'bitgo nitro', email: 'bitgo@test.com', }, ], }); const nockGPGKey = await nockGetBitgoPublicKeyBasedOnFeatureFlags(coinName, 'enterprise_id', nitroGPGKeypair); const bitgoGpgPublicKey = await tssUtils.getBitgoGpgPubkeyBasedOnFeatureFlags('enterprise_id'); should.equal(nockGPGKey.publicKey, bitgoGpgPublicKey.armor()); }); it('getBackupEncryptedNShare should get valid encrypted n shares based on provider', async function () { const bitgoGpgKeyPubKey = await tssUtils.getBitgoPublicGpgKey(); // Backup key held by user const backupShareHolderNew = { userHeldKeyShare: backupKeyShare, }; const backupToBitgoEncryptedNShare = await tssUtils.getBackupEncryptedNShare(backupShareHolderNew, 3, bitgoGpgKeyPubKey.armor(), userGpgKey); const encryptedNShare = await encryptNShare(backupKeyShare, 3, bitgoGpgKeyPubKey.armor(), userGpgKey); // cant verify the encrypted shares, since they will be encrypted with diff. values should.equal(backupToBitgoEncryptedNShare.publicShare, encryptedNShare.publicShare); }); it('should generate TSS key chains', async function () { const backupShareHolder = { userHeldKeyShare: backupKeyShare, }; const backupGpgKey = userLocalBackupGpgKey; const bitgoKeychain = await tssUtils.createBitgoKeychain({ userGpgKey, backupGpgKey, userKeyShare, backupKeyShare: backupShareHolder, bitgoPublicGpgKey: bitgoPublicKey, }); const usersKeyChainPromises = [ tssUtils.createParticipantKeychain(userGpgKey, userLocalBackupGpgKey, bitgoPublicKey, 1, userKeyShare, backupKeyShare, bitgoKeychain, 'passphrase'), tssUtils.createParticipantKeychain(userGpgKey, userLocalBackupGpgKey, bitgoPublicKey, 2, userKeyShare, backupKeyShare, bitgoKeychain, 'passphrase'), ]; const [userKeychain, backupKeychain] = await Promise.all(usersKeyChainPromises); bitgoKeychain.should.deepEqual(nockedBitGoKeychain); userKeychain.should.deepEqual(nockedUserKeychain); // unencrypted `prv` property should exist on backup keychain const keyChainPrv = JSON.parse(backupKeychain.prv ?? ''); _.isEqual(keyChainPrv.pShare, backupKeyShare.pShare).should.be.true(); _.isEqual(keyChainPrv.bitgoNShare, bitgoKeyShare.nShares[2]).should.be.true(); _.isEqual(keyChainPrv.userNShare, userKeyShare.nShares[2]).should.be.true(); should.exist(backupKeychain.encryptedPrv); }); it('should generate TSS key chains with optional params', async function () { const enterprise = 'enterprise_id'; const backupShareHolder = { userHeldKeyShare: backupKeyShare, }; const backupGpgKey = userLocalBackupGpgKey; const bitgoKeychain = await tssUtils.createBitgoKeychain({ userGpgKey, backupGpgKey, userKeyShare, backupKeyShare: backupShareHolder, enterprise, bitgoPublicGpgKey: bitgoPublicKey, }); const usersKeyChainPromises = [ tssUtils.createParticipantKeychain(userGpgKey, userLocalBackupGpgKey, bitgoPublicKey, 1, userKeyShare, backupKeyShare, bitgoKeychain, 'passphrase', 'originalPasscodeEncryptionCode'), tssUtils.createParticipantKeychain(userGpgKey, userLocalBackupGpgKey, bitgoPublicKey, 2, userKeyShare, backupKeyShare, bitgoKeychain, 'passphrase'), ]; const [userKeychain, backupKeychain] = await Promise.all(usersKeyChainPromises); bitgoKeychain.should.deepEqual(nockedBitGoKeychain); userKeychain.should.deepEqual(nockedUserKeychain); // unencrypted `prv` property should exist on backup keychain const keyChainPrv = JSON.parse(backupKeychain.prv ?? ''); _.isEqual(keyChainPrv.pShare, backupKeyShare.pShare).should.be.true(); _.isEqual(keyChainPrv.bitgoNShare, bitgoKeyShare.nShares[2]).should.be.true(); _.isEqual(keyChainPrv.userNShare, userKeyShare.nShares[2]).should.be.true(); should.exist(backupKeychain.encryptedPrv); }); it('should fail to generate TSS key chains', async function () { const backupShareHolder = { userHeldKeyShare: backupKeyShare, }; const backupGpgKey = userLocalBackupGpgKey; const bitgoKeychain = await tssUtils.createBitgoKeychain({ userGpgKey, backupGpgKey, userKeyShare, backupKeyShare: backupShareHolder, bitgoPublicGpgKey: bitgoPublicKey, }); bitgoKeychain.should.deepEqual(nockedBitGoKeychain); const testKeyShares = ecdsaFixtures_1.otherKeyShares; const testCasesPromises = [ tssUtils .createParticipantKeychain(userGpgKey, userLocalBackupGpgKey, bitgoPublicKey, 1, userKeyShare, testKeyShares[0], bitgoKeychain, 'passphrase') .should.be.rejectedWith('Common keychains do not match'), tssUtils .createParticipantKeychain(userGpgKey, userLocalBackupGpgKey, bitgoPublicKey, 1, testKeyShares[1], backupKeyShare, bitgoKeychain, 'passphrase') .should.be.rejectedWith('Common keychains do not match'), tssUtils .createParticipantKeychain(userGpgKey, userLocalBackupGpgKey, bitgoPublicKey, 2, testKeyShares[2], backupKeyShare, bitgoKeychain, 'passphrase') .should.be.rejectedWith('Common keychains do not match'), tssUtils .createParticipantKeychain(userGpgKey, userLocalBackupGpgKey, bitgoPublicKey, 2, userKeyShare, testKeyShares[3], bitgoKeychain, 'passphrase') .should.be.rejectedWith('Common keychains do not match'), ]; await Promise.all(testCasesPromises); }); it('should fail to generate TSS keychains when received invalid number of wallet signatures', async function () { const bitgoKeychain = await generateBitgoKeychain({ coin: coinName, userKeyShare, backupKeyShare, bitgoKeyShare, userGpgKey, userLocalBackupGpgKey, bitgoGpgKey: bitGoGPGKeyPair, }); const certsString = await (0, src_1.createSharedDataProof)(bitGoGPGKeyPair.privateKey, userGpgKey.publicKey, []); const certsKey = await openpgp.readKey({ armoredKey: certsString }); const finalKey = new openpgp.PacketList(); certsKey.toPacketList().forEach((packet) => finalKey.push(packet)); // Once the following PR has been merged and released we no longer need the ts-ignore: // https://github.com/openpgpjs/openpgpjs/pull/1576 // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore bitgoKeychain.walletHSMGPGPublicKeySigs = openpgp.armor(openpgp.enums.armor.publicKey, finalKey.write()); await tssUtils .verifyWalletSignatures(userLocalBackupGpgKey.publicKey, userLocalBackupGpgKey.publicKey, bitgoKeychain, '', 1) .should.be.rejectedWith(`Invalid wallet signatures`); }); it('should fail to generate TSS keychains when wallet signature fingerprints do not match passed user/backup fingerprints', async function () { const customUserKeyShare = await MPC.keyShare(1, 2, 3); const customBackupKeyShare = await MPC.keyShare(2, 2, 3); const backupShareHolder = { userHeldKeyShare: customBackupKeyShare, }; const backupGpgKey = userLocalBackupGpgKey; const bitgoKeychain = await tssUtils.createBitgoKeychain({ userGpgKey, backupGpgKey, userKeyShare: customUserKeyShare, backupKeyShare: backupShareHolder, bitgoPublicGpgKey: bitgoPublicKey, }); // using the backup gpg here instead of the user gpg key to simulate that the first signature has a different // fingerprint from the passed in first gpg key await tssUtils .verifyWalletSignatures(userLocalBackupGpgKey.publicKey, userLocalBackupGpgKey.publicKey, bitgoKeychain, '', 1) .should.be.rejectedWith(`first wallet signature's fingerprint does not match passed user gpg key's fingerprint`); // using the user gpg here instead of the backup gpg key to simulate that the second signature has a different // fingerprint from the passed in second gpg key await tssUtils .verifyWalletSignatures(userGpgKey.publicKey, userGpgKey.publicKey, bitgoKeychain, '', 1) .should.be.rejectedWith(`second wallet signature's fingerprint does not match passed backup gpg key's fingerprint`); }); }); describe('signTxRequest:', () => { const txRequestId = 'randomidEcdsa'; const txRequest = { txRequestId, transactions: [ { unsignedTx: { // hteth txid: 0xc5a7bfe6b13ceae563da0f9feaa9c4ad1c101a15366a2a488828a5dd27cb9da3 serializedTxHex: '02f38242688084448b9b8084448b9b908301637894a1cfb9d51c0af191ff21c5f0f01723e056f7dc12865af3107a400080c0808080', signableHex: '02f08242688084448b9b8084448b9b908301637894a1cfb9d51c0af191ff21c5f0f01723e056f7dc12865af3107a400080c0', derivationPath: '', // Needs this when key derivation is supported }, state: 'pendingSignature', signatureShares: [], }, ], unsignedTxs: [ { // hteth txid: 0xc5a7bfe6b13ceae563da0f9feaa9c4ad1c101a15366a2a488828a5dd27cb9da3 serializedTxHex: '02f38242688084448b9b8084448b9b908301637894a1cfb9d51c0af191ff21c5f0f01723e056f7dc12865af3107a400080c0808080', signableHex: '02f38242688084448b9b8084448b9b908301637894a1cfb9d51c0af191ff21c5f0f01723e056f7dc12865af3107a400080c0808080', derivationPath: '', // Needs this when key derivation is supported }, ], date: new Date().toISOString(), intent: { intentType: 'payment', }, latest: true, state: 'pendingUserSignature', walletType: 'hot', walletId: 'walletId', policiesChecked: true, version: 1, userId: 'userId', }; let aShare, dShare, wShare, oShare, userSignShare, bitgoChallenges, enterpriseChallenges; beforeEach(async () => { // Initializing user and bitgo for creating shares for nocks const userSigningKey = MPC.keyCombine(userKeyShare.pShare, [bitgoKeyShare.nShares[1], backupKeyShare.nShares[1]]); const bitgoSigningKey = MPC.keyCombine(bitgoKeyShare.pShare, [ userKeyShare.nShares[3], backupKeyShare.nShares[3], ]); const serializedEntChallenge = ecdsaNtilde_1.mockChallengeA; const serializedBitgoChallenge = ecdsaNtilde_1.mockChallengeB; const deserializedEntChallenge = sdk_lib_mpc_1.EcdsaTypes.deserializeNtildeWithProofs(serializedEntChallenge); sinon.stub(sdk_lib_mpc_1.EcdsaRangeProof, 'generateNtilde').resolves(deserializedEntChallenge); const [userToBitgoPaillierChallenge, bitgoToUserPaillierChallenge] = await Promise.all([ sdk_lib_mpc_1.EcdsaPaillierProof.generateP((0, sdk_lib_mpc_1.hexToBigInt)(userSigningKey.yShares[3].n)), sdk_lib_mpc_1.EcdsaPaillierProof.generateP((0, sdk_lib_mpc_1.hexToBigInt)(bitgoSigningKey.yShares[1].n)), ]); bitgoChallenges = { ...serializedBitgoChallenge, p: sdk_lib_mpc_1.EcdsaTypes.serializePaillierChallenge({ p: bitgoToUserPaillierChallenge }).p, n: bitgoSigningKey.xShare.n, }; enterpriseChallenges = { ...serializedEntChallenge, p: sdk_lib_mpc_1.EcdsaTypes.serializePaillierChallenge({ p: userToBitgoPaillierChallenge }).p, n: bitgoSigningKey.xShare.n, }; sinon.stub(sdk_core_1.ECDSAUtils.EcdsaUtils.prototype, 'getEcdsaSigningChallenges').resolves({ enterpriseChallenge: enterpriseChallenges, bitgoChallenge: bitgoChallenges, }); const [userXShare, bitgoXShare] = [ MPC.appendChallenge(userSigningKey.xShare, serializedEntChallenge, sdk_lib_mpc_1.EcdsaTypes.serializePaillierChallenge({ p: userToBitgoPaillierChallenge })), MPC.appendChallenge(bitgoSigningKey.xShare, serializedBitgoChallenge, sdk_lib_mpc_1.EcdsaTypes.serializePaillierChallenge({ p: bitgoToUserPaillierChallenge })), ]; const bitgoYShare = MPC.appendChallenge(userSigningKey.yShares[3], serializedBitgoChallenge, sdk_lib_mpc_1.EcdsaTypes.serializePaillierChallenge({ p: bitgoToUserPaillierChallenge })); /** * START STEP ONE * 1) User creates signShare, saves wShare and sends kShare to bitgo * 2) Bitgo performs signConvert operation using its private xShare , yShare * and KShare from user and responds back with aShare and saves bShare for later use */ userSignShare = await sdk_core_1.ECDSAMethods.createUserSignShare(userXShare, bitgoYShare); wShare = userSignShare.wShare; const signatureShareOneFromUser = { from: sdk_core_1.SignatureShareType.USER, to: sdk_core_1.SignatureShareType.BITGO, share: sdk_core_1.ECDSAMethods.convertKShare(userSignShare.kShare).share.replace(sdk_core_1.ECDSAMethods.delimeter, ''), }; const getBitgoAandBShare = await MPC.signConvertStep1({ xShare: bitgoXShare, yShare: bitgoSigningKey.yShares[1], // corresponds to the user kShare: userSignShare.kShare, }); const bitgoAshare = getBitgoAandBShare.aShare; aShare = bitgoAshare; const aShareBitgoResponse = sdk_core_1.ECDSAMethods.convertAShare(bitgoAshare).share.replace(sdk_core_1.ECDSAMethods.delimeter, ''); const signatureShareOneFromBitgo = { from: sdk_core_1.SignatureShareType.BITGO, to: sdk_core_1.SignatureShareType.USER, share: aShareBitgoResponse, }; await (0, common_1.nockSendSignatureShareWithResponse)({ walletId: wallet.id(), txRequestId: txRequest.txRequestId, signatureShare: signatureShareOneFromUser, response: signatureShareOneFromBitgo, tssType: 'ecdsa', }); /** END STEP ONE */ /** * START STEP TWO * 1) Using the aShare got from bitgo and wShare from previous step, * user creates gShare and muShare and sends muShare to bitgo * 2) Bitgo using the signConvert step using bShare from previous step * and muShare from user generates its gShare. * 3) Using the signCombine operation using gShare, Bitgo generates oShare * which it saves and dShare which is send back to the user. */ const userGammaAndMuShares = await sdk_core_1.ECDSAMethods.createUserGammaAndMuShare(userSignShare.wShare, bitgoAshare); const signatureShareTwoFromUser = { from: sdk_core_1.SignatureShareType.USER, to: sdk_core_1.SignatureShareType.BITGO, share: sdk_core_1.ECDSAMethods.convertMuShare(userGammaAndMuShares.muShare).share.replace(sdk_core_1.ECDSAMethods.delimeter, ''), }; const getBitGoGShareAndSignerIndexes = await MPC.signConvertStep3({ bShare: getBitgoAandBShare.bShare, muShare: userGammaAndMuShares.muShare, }); const getBitgoOShareAndDShares = MPC.signCombine({ gShare: getBitGoGShareAndSignerIndexes.gShare, signIndex: { i: 1, j: 3, }, }); const bitgoDshare = getBitgoOShareAndDShares.dShare; dShare = bitgoDshare; const dShareBitgoResponse = bitgoDshare.delta + bitgoDshare.Gamma; const signatureShareTwoFromBitgo = { from: sdk_core_1.SignatureShareType.BITGO, to: sdk_core_1.SignatureShareType.USER, share: dShareBitgoResponse, }; await (0, common_1.nockSendSignatureShareWithResponse)({ walletId: wallet.id(), txRequestId: txRequest.txRequestId, signatureShare: signatureShareTwoFromUser, response: signatureShareTwoFromBitgo, tssType: 'ecdsa', }); /** END STEP TWO */ /** * START STEP THREE * 1) User creates its oShare and dShare using the private gShare * from step two * 2) User uses the private oShare and dShare from bitgo from step * two to generate its signature share which it sends back along with dShare that * user generated from the above step * 3) Bitgo using its private oShare from step two and dShare from bitgo creates * its signature share. Using the Signature Share received from user from the above * step, bitgo constructs the final signature and is returned to the user */ const userOmicronAndDeltaShare = await sdk_core_1.ECDSAMethods.createUserOmicronAndDeltaShare(userGammaAndMuShares.gShare); oShare = userOmicronAndDeltaShare.oShare; const signablePayload = Buffer.from(txRequest.unsignedTxs[0].signableHex, 'hex'); const userSShare = await sdk_core_1.ECDSAMethods.createUserSignatureShare(userOmicronAndDeltaShare.oShare, bitgoDshare, signablePayload); const signatureShareThreeFromUser = { from: sdk_core_1.SignatureShareType.USER, to: sdk_core_1.SignatureShareType.BITGO, share: userSShare.R + userSShare.s + userSShare.y + userOmicronAndDeltaShare.dShare.delta + userOmicronAndDeltaShare.dShare.Gamma, }; const getBitGoSShare = MPC.sign(signablePayload, getBitgoOShareAndDShares.oShare, userOmicronAndDeltaShare.dShare, createKeccakHash('keccak256')); const getBitGoFinalSignature = MPC.constructSignature([getBitGoSShare, userSShare]); const finalSigantureBitgoResponse = getBitGoFinalSignature.r + getBitGoFinalSignature.s + getBitGoFinalSignature.y; const signatureShareThreeFromBitgo = { from: sdk_core_1.SignatureShareType.BITGO, to: sdk_core_1.SignatureShareType.USER, share: finalSigantureBitgoResponse, }; await (0, common_1.nockSendSignatureShareWithResponse)({ walletId: wallet.id(), txRequestId: txRequest.txRequestId, signatureShare: signatureShareThreeFromUser, response: signatureShareThreeFromBitgo, tssType: 'ecdsa', }); /* END STEP THREE */ const signature = MPC.constructSignature([userSShare, getBitGoSShare]); MPC.verify(signablePayload, signature, createKeccakHash('keccak256')).should.be.true; }); afterEach(async () => { sinon.restore(); }); it('signTxRequest should fail if wallet is in pendingEcdsaTssInitialization', async function () { sandbox.stub(wallet, 'coinSpecific').returns({ customChangeWalletId: '', pendingEcdsaTssInitialization: true, }); await tssUtils .signTxRequest({ txRequest, prv: JSON.stringify({ pShare: userKeyShare.pShare, bitgoNShare: bitgoKeyShare.nShares[1], backupNShare: backupKeyShare.nShares[1], }), reqId, }) .should.be.rejectedWith('Wallet is not ready for TSS ECDSA signing. Please contact your enterprise admin to finish the enterprise TSS initialization.'); }); it('signTxRequest should succeed with txRequest object as input', async function () { const sendShareSpy = sinon.spy(sdk_core_1.ECDSAMethods, 'sendShareToBitgo'); await setupSignTxRequestNocks(false, userSignShare, aShare, dShare, enterpriseData); const signedTxRequest = await tssUtils.signTxRequest({ txRequest, prv: JSON.stringify({ pShare: userKeyShare.pShare, bitgoNShare: bitgoKeyShare.nShares[1], backupNShare: backupKeyShare.nShares[1], }), reqId, }); signedTxRequest.unsignedTxs.should.deepEqual(txRequest.unsignedTxs); const userGpgActual = sendShareSpy.getCalls()[0].args[10]; userGpgActual.should.startWith('-----BEGIN PGP PUBLIC KEY BLOCK-----'); }); it('signTxRequest should succeed with txRequest id as input', async function () { const sendShareSpy = sinon.spy(sdk_core_1.ECDSAMethods, 'sendShareToBitgo'); await setupSignTxRequestNocks(true, userSignShare, aShare, dShare, enterpriseData); const signedTxRequest = await tssUtils.signTxRequest({ txRequest: txRequestId, prv: JSON.stringify({ pShare: userKeyShare.pShare, bitgoNShare: bitgoKeyShare.nShares[1], backupNShare: backupKeyShare.nShares[1], }), reqId, }); signedTxRequest.unsignedTxs.should.deepEqual(txRequest.unsignedTxs); const userGpgActual = sendShareSpy.getCalls()[0].args[10]; userGpgActual.should.startWith('-----BEGIN PGP PUBLIC KEY BLOCK-----'); }); it('signTxRequest should fail with wrong recipient', async function () { // To generate these Hex values, we used the bitgo-ui to create a transaction and then // used the `signableHex` and `serializedTxHex` values from the prebuild. const signableHex = '02f283088bb0038406f18758847f3443c683016378949726ea58be57b153791a8ff5b282d776aaf28e3c87038d7ea4c6800080c0'; const serializedTxHex = '02f583088bb0038406f18758847f3443c683016378949726ea58be57b153791a8ff5b282d776aaf28e3c87038d7ea4c6800080c0808080'; await setupSignTxRequestNocks(true, userSignShare, aShare, dShare, enterpriseData, { signableHex, serializedTxHex, apiVersion: 'full', }); await tssUtils .signTxRequest({ txRequest: txRequestId, prv: JSON.stringify({ pShare: userKeyShare.pShare, bitgoNShare: bitgoKeyShare.nShares[1], backupNShare: backupKeyShare.nShares[1], }), reqId, txParams: { recipients: [{ address: '0x1234', amount: '1000000000000000' }], type: 'transfer' }, }) .should.be.rejectedWith('destination address does not match with the recipient address'); }); it('signTxRequest should fail with incorrect value', async function () { nock.cleanAll(); // To generate these Hex values, we used the bitgo-ui to create a transaction and then // used the `signableHex` and `serializedTxHex` values from the prebuild. const signableHex = '02f283088bb0038406f18758847f3443c683016378949726ea58be57b153791a8ff5b282d776aaf28e3c87038d7ea4c6800080c0'; const serializedTxHex = '02f583088bb0038406f18758847f3443c683016378949726ea58be57b153791a8ff5b282d776aaf28e3c87038d7ea4c6800080c0808080'; await setupSignTxRequestNocks(true, userSignShare, aShare, dShare, enterpriseData, { signableHex, serializedTxHex, apiVersion: 'full', }); await tssUtils .signTxRequest({ txRequest: txRequestId, prv: JSON.stringify({ pShare: userKeyShare.pShare, bitgoNShare: bitgoKeyShare.nShares[1], backupNShare: backupKeyShare.nShares[1], }), reqId, txParams: { recipients: [{ address: '0xa1cfb9d51c0af191ff21c5f0f01723e056f7dc12', amount: '1' }], type: 'transfer', }, }) .should.be.rejectedWith('the transaction amount in txPrebuild does not match the value given by client'); }); it('signTxRequest should fail with incorrect value for token txn', async function () { nock.cleanAll(); // To generate these Hex values, we used the bitgo-ui to create a transaction and then // used the `signableHex` and `serializedTxHex` values from the prebuild. const signableHex = '02f86f83088bb00283e1d7dd84768ea6898301e04b94d9327fd36c3312466efed23ff0493453ee32f55180b844a9059cbb0000000000000000000000007d7e63af583ba73ba5c927dbd028153963566bef00000000000000000000000000000000000000000000000000470de4df820000c0'; const serializedTxHex = '02f87283088bb00283e1d7dd84768ea6898301e04b94d9327fd36c3312466efed23ff0493453ee32f55180b844a9059cbb0000000000000000000000007d7e63af583ba73ba5c927dbd028153963566bef00000000000000000000000000000000000000000000000000470de4df820000c0808080'; await setupSignTxRequestNocks(true, userSignShare, aShare, dShare, enterpriseData, { signableHex, serializedTxHex, apiVersion: 'full', }); await tssUtils .signTxRequest({ txRequest: txRequestId, prv: JSON.stringify({ pShare: userKeyShare.pShare, bitgoNShare: bitgoKeyShare.nShares[1], backupNShare: backupKeyShare.nShares[1], }), reqId, txParams: { recipients: [{ address: '0x2b0d6cb2f8c388757f4d7ad857fccab18290dbc9', amount: '707' }], type: 'transfer', }, }) .should.be.rejectedWith('the transaction amount in txPrebuild does not match the value given by client'); }); it('signTxRequest should succeed for WalletConnect ERC20 transfer with data field', async function () { nock.cleanAll(); // WalletConnect ERC20 transfer: recipients[0].address is token contract, recipients[0].amount is 0 (no native coins) // recipients[0].data contains the actual recipient and token amount const signableHex = '02f86f83088bb00283e1d7dd84768ea6898301e04b94d9327fd36c3312466efed23ff0493453ee32f55180b844a9059cbb0000000000000000000000007d7e63af583ba73ba5c927dbd028153963566bef00000000000000000000000000000000000000000000000000470de4df820000c0'; const serializedTxHex = '02f87283088bb00283e1d7dd84768ea6898301e04b94d9327fd36c3312466efed23ff0493453ee32f55180b844a9059cbb0000000000000000000000007d7e63af583ba73ba5c927dbd028153963566bef00000000000000000000000000000000000000000000000000470de4df820000c0808080'; await setupSignTxRequestNocks(true, userSignShare, aShare, dShare, enterpriseData, { signableHex, serializedTxHex, apiVersion: 'full', }); await tssUtils.signTxRequest({ txRequest: txRequestId, prv: JSON.stringify({ pShare: userKeyShare.pShare, bitgoNShare: bitgoKeyShare.nShares[1], backupNShare: backupKeyShare.nShares[1], }), reqId, txParams: { recipients: [ { address: '0xd9327fd36c3312466efed23ff0493453ee32f551', // Token contract address amount: '0', // No native coins sent data: '0xa9059cbb0000000000000000000000007d7e63af583ba73ba5c927dbd028153963566bef00000000000000000000000000000000000000000000000000470de4df820000', // ERC20 transfer calldata with actual recipient and token amount }, ], type: 'transfer', }, }); }); it('getOfflineSignerPaillierModulus should succeed', async function () { const paillierModulus = tssUtils.getOfflineSignerPaillierModulus({ prv: JSON.stringify({ pShare: userKeyShare.pShare, bitgoNShare: bitgoKeyShare.nShares[1], backupNShare: backupKeyShare.nShares[1], }), }); paillierModulus.userPaillierModulus.should.equal(userKeyShare.pShare.n); }); it('createOfflineKShare should succeed', async function () { const mockPassword = 'password'; const step1SigningMaterial = await tssUtils.createOfflineKShare({ tssParams: { txRequest, reqId: reqId, }, challenges: { enterpriseChallenge: enterpriseChallenges, bitgoChallenge: bitgoChallenges, }, prv: JSON.stringify({ pShare: userKeyShare.pShare, bitgoNShare: bitgoKeyShare.nShares[1], backupNShare: backupKeyShare.nShares[1], }), requestType: src_1.RequestType.tx, walletPassphrase: mockPassword, }); step1SigningMaterial.privateShareProof.should.startWith('-----BEGIN PGP PUBLIC KEY BLOCK-----'); step1SigningMaterial.vssProof?.length.should.equal(userKeyShare.nShares[3].v?.length); step1SigningMaterial.publicShare.length.should.equal(userKeyShare.nShares[3].y.length + userKeyShare.nShares[3].chaincode.length); step1SigningMaterial.encryptedSignerOffsetShare.should.startWith('-----BEGIN PGP MESSAGE-----'); step1SigningMaterial.userPublicGpgKey.should.startWith('-----BEGIN PGP PUBLIC KEY BLOCK-----'); step1SigningMaterial.kShare.n.should.equal(userKeyShare.pShare.n); step1SigningMaterial.wShare.should.startWith('{"iv":'); }); it('createOfflineKShare should fail with txId passed', async function () { const mockPassword = 'password'; await tssUtils .createOfflineKShare({ tssParams: { txRequest: txRequest.txRequestId, reqId: reqId, }, challenges: { enterpriseChallenge: enterpriseChallenges, bitgoChallenge: bitgoChallenges, }, prv: JSON.stringify({ pShare: userKeyShare.pShare, bitgoNShare: bitgoKeyShare.nShares[1], backupNShare: backupKeyShare.nShares[1], }), requestType: src_1.RequestType.tx, walletPassphrase: mockPassword, }) .should.be.rejectedWith('Invalid txRequest type'); }); // Seems to be flaky on CI, failed here: https://github.com/BitGo/BitGoJS/actions/runs/5902489990/job/16010623888?pr=3822 xit('createOfflineMuDeltaShare should succeed', async function () { const mockPassword = 'password'; const alphaLength = 1536; const deltaLength = 64; const bitgo = sdk_test_1.TestBitGo.decorate(src_1.BitGo, { env: 'mock' }); const step2SigningMaterial = await tssUtils.createOfflineMuDeltaShare({ aShareFromBitgo: aShare, bitgoChallenge: bitgoChallenges, encryptedWShare: bitgo.encrypt({ input: JSON.stringify(wShare), password: mockPassword }), walletPassphrase: mockPassword, }); step2SigningMaterial.muDShare.muShare.alpha.length.should.equal(alphaLength); step2SigningMaterial.muDShare.dShare.delta.length.should.equal(deltaLength); step2SigningMaterial.oShare.should.startWith('{"iv":'); }); it('createOfflineMuDeltaShare should fail with incorrect password', async function () { const mockPassword = 'password'; const bitgo = sdk_test_1.TestBitGo.decorate(src_1.BitGo, { env: 'mock' }); await tssUtils .createOfflineMuDeltaShare({ aShareFromBitgo: aShare, bitgoChallenge: bitgoChallenges, encryptedWShare: bitgo.encrypt({ input: JSON.stringify(wShare), password: mockPassword }), walletPassphrase: 'password1', }) .should.be.rejectedWith("password error - ccm: tag doesn't match"); }); it('createOfflineSShare should succeed', async function () { const mockPassword = 'password'; const pubKeyLength = 66; const privKeyLength = 64; const bitgo = sdk_test_1.TestBitGo.decorate(src_1.BitGo, { env: 'mock' }); const step3SigningMaterial = await tssUtils.createOfflineSShare({ tssParams: { txRequest: txRequest, reqId: reqId, }, dShareFromBitgo: dShare, encryptedOShare: bitgo.encrypt({ input: JSON.stringify(oShare), password: mockPassword }), walletPassphrase: mockPassword, requestType: src_1.RequestType.tx, }); step3SigningMaterial.R.length.should.equal(pubKeyLength); step3SigningMaterial.y.length.should.equal(pubKeyLength); step3SigningMaterial.s.length.should.equal(privKeyLength); }); it('createOfflineSShare should fail with txId passed', async function () { const mockPassword = 'password'; const bitgo = sdk_test_1.TestBitGo.decorate(src_1.BitGo, { env: 'mock' }); await tssUtils .createOfflineSShare({ tssParams: { txRequest: txRequest.txRequestId, reqId: reqId, }, dShareFromBitgo: dShare, encryptedOShare: bitgo.encrypt({ input: JSON.stringify(oShare), password: mockPassword }), walletPassphrase: mockPassword, requestType: src_1.RequestType.tx, }) .should.be.rejectedWith('Invalid txRequest type'); }); it('signTxRequest should fail with invalid user prv', async function () { const invalidUserKey = { ...userKeyShare, pShare: { ...userKeyShare.pShare, i: 2 } }; await tssUtils .signTxRequest({ txRequest: txRequestId, prv: JSON.stringify({ pShare: invalidUserKey.pShare, bitgoNShare: bitgoKeyShare.nShares[1], backupNShare: backupKeyShare.nShares[1], }), reqId, }) .should.be.rejectedWith('Invalid user key'); }); it('signTxRequest should fail with no backupNShares', async function () { const getTxRequest = sandbox.stub(tssUtils, 'getTxRequest'); getTxRequest.resolves(txRequest); getTxRequest.calledWith(txRequestId); setupSignTxRequestNocks(false, userSignShare, aShare, dShare, enterpriseData); await tssUtils .signTxRequest({ txRequest: txRequestId, prv: JSON.stringify({ pShare: userKeyShare.pShare, bitgoNShare: bitgoKeyShare.nShares[1], }), reqId, }) .should.be.rejectedWith('Invalid user key - missing backupNShare'); }); async function setupSignTxRequestNocks(isTxRequest = true, userSignShare, aShare, dShare, enterpriseData, { signableHex, serializedTxHex, apiVersion, } = {}) { if (enterpriseData) { await (0, helpers_1.nockGetEnterprise)({ enterpriseId: enterpriseData.id, response: enterpriseData, times: 1 }); } const derivationPath = ''; sinon.stub(sdk_core_1.ECDSAMethods, 'createUserSignShare').resolves(userSignShare); let response = { txRequests: [ { ...txRequest, transactions: [ { ...txRequest, unsignedTx: { signableHex: signableHex ?? txRequest.unsignedTxs[0].signableHex, serializedTxHex: serializedTxHex ?? txRequest.unsignedTxs[0].serializedTxHex, derivationPath, }, }, ], apiVersion: apiVersion, }, ], }; if (isTxRequest) { await (0, helpers_1.nockGetTxRequest)({ walletId: wallet.id(), txRequestId: txRequest.txRequestId, response: response }); } const aRecord = sdk_core_1.ECDSAMethods.convertAShare(aShare); const signatureShares = [aRecord]; txRequest.signatureShares = signatureShares; response = { txRequests: [ { ...txRequest, transactions: [ { ...txRequest, unsignedTx: { signableHex: txRequest.unsignedTxs[0].signableHex, serializedTxHex: txRequest.unsignedTxs[0].serializedTxHex, derivationPath, }, }, ], apiVersion: apiVersion, }, ], }; await (0, helpers_1.nockGetTxRequest)({ walletId: wallet.id(), txRequestId: txRequest.txRequestId, response: response }); const dRecord = sdk_core_1.ECDSAMethods.convertDShare(dShare); signatureShares.push(dRecord); response = { txRequests: [ { ...txRequest, transactions: [ { ...txRequest, unsignedTx: { signableHex: txRequest.unsignedTxs[0].signableHex, serializedTxHex: txRequest.unsignedTxs[0].serializedTxHex, derivationPath, }, }, ], apiVersion: apiVersion, }, ], }; await (0, helpers_1.nockGetTxRequest)({ walletId: wallet.id(), txRequestId: txRequest.txRequestId, response: response }); await (0, helpers_1.nockGetTxRequest)({ walletId: wallet.id(), txRequestId: txRequest.txRequestId, response: response }); } }); describe('getEcdsaSigningChallenges', function () { const mockWalletPaillierKey = { n: 'f47be4c2d8bc1e28f88c6c4da634da97d92a1c279a7b0fe7b87c337c36a27b32ce0ff0c45f16e4e15bbd20e4e640de12047eff9b1a2b98144f9a268d406bd000d192a35b6847a17e40fb85f55b314d001ff87393481cafe391807d0eb83eff9e38614b38e5f25fc4449cb01caed805584d026b5d866c723f3d4d4f1e462662f2113b1561eb2bf755b4b91d0308d8eacc439167da8b7d6e108524f226960360af00215d9614457414ebdbe8834999689e2e903208c8713ff5d9901f9eaba3aa81d705323cbbba61ba7fa9f3228f30853fb55da1b3d3ed7db1dfc6545bc96aa8d2eb848931c1b807fdfe8f65af72f68638a82fe9e22ac1f0f032e621066806a1f144b5719a5f091986867b384be6c34146c8241cbfbd781966ebbcd19e6caa27fab040e62e5a162888aa8624d046c8fe3b72244f04a7264c4a36b6366dbe7da98afb201d34be2c0d6dd11982af35bf7535582b263914725aaec280d52290527382d3ab297d746c41aacd8de98c09fcfb85a95e02de1b34d4933e51045e2f1ce8af', lambda: 'f47be4c2d8bc1e28f88c6c4da634da97d92a1c279a7b0fe7b87c337c36a27b32ce0ff0c45f16e4e15bbd20e4e640de12047eff9b1a2b98144f9a268d406bd000d192a35b6847a17e40fb85f55b314d001ff87393481cafe391807d0eb83eff9e38614b38e5f25fc4449cb01caed805584d026b5d866c723f3d4d4f1e462662f2113b1561eb2bf755b4b91d0308d8eacc439167da8b7d6e108524f226960360af00215d9614457414ebdbe8834999689e2e903208c8713ff5d9901f9eaba3aa7fc3d0c0bcc5bff644156ab887146d51bcee1eef70f45c486147d687ee37def1f8a16bc945eff22dd4dca3614a99158823acd9492e347f7ec79a7771024205d07f27b30cd20340e330411da8fa2da209e5cc688da94d1dbef54bfd9c69b4e99cf06d67309a3420b82c78a0fe0dd0b9c31382eae38746cfdd27fa90022a50532246c8ae1339c93e183c03bf6fd7014be3658abc73baae1fa5b86dab94b9f125395a818e54dde6235c45d3dbc032b3078e9df1cad69d8ac19a7cb6405a558b7bfba8', }; const mockBitgoPaillierKey = { n: 'f010d294effceb8c4f96af1978ab367c4fbb272c2169317e41ae87220652cae2ce929696ee55ec6831aa6b4b3b931babc2bac9c1a20fddbca925cc99680791f7c3157b3d31256ee72c47d47db567e0f070dce121c3a4d9e003c1f1389073acb252c65d2b0723e86e3265f67a137cb1e23f4551544405644d0ae63d35f25f40becd2b693879f3bdbec3f7250791a3f3c975a5ac78a0e81dcd1a87eb2ca67010dff880b2338556275de23d9e88d21b77da0d524ddc2b394f8de00b1af0ce85f6eee2e05a184e05494d66d2c636045bf70ed15ebd0f41a8eea2920af85e6d68a0ce11fc2abbcb3cebcc3c23ec2e148c318683a5426e15b5207efd3b9b05cb919ec4340f74dff336986d0c923df10a789007b1da9da