bitgo
Version:
BitGo JavaScript SDK
885 lines • 252 kB
JavaScript
"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