UNPKG

bitgo

Version:
1,100 lines 155 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const sodium = require("libsodium-wrappers-sumo"); 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 helpers_1 = require("../../tss/helpers"); const common_1 = require("./common"); openpgp.config.rejectCurves = new Set(); describe('TSS Utils:', async function () { let sandbox; let MPC; let bgUrl; let tssUtils; let userGpgKey; let backupGpgKey; let bitgoGpgKey; let bitgo; let baseCoin; let wallet; let bitgoKeyShare; const reqId = new sdk_core_1.RequestTracer(); const coinName = 'tsol'; const validUserSigningMaterial = { uShare: { i: 1, t: 2, n: 3, y: '093c8603ad86c41d5ee25a814b88185b435dd3a9ceccf9c9fd691a465ac4a8b0', seed: 'ca40c789813250c334ddd2ba19050f6ed20b5a08853ceca492358f2711ad4b15', chaincode: '596d5404a7eb918ee78247b952d06539619884091fdd9e0ff5a665f349e32fca', }, commonChaincode: '596d5404a7eb918ee78247b952d06539619884091fdd9e0ff5a665f349e32fca', bitgoYShare: { i: 1, j: 3, y: '59d8000ba5e85fa402f39382960e7d5ede82b1b6e22b146a18b7df238c3a3225', v: '01ea3f425b1adf8aec6cfe4fc8f9b94755c34657965f32397655dcd784f1b517', u: '9ce3204a8c9757738967f3f81b463d87267bf6f2c0e5eaf2843167537b872b0b', chaincode: 'd21dbd8eae5d4789292ecea2efa53e0165b2439d57f5158eb4dd57dc26b59236', }, backupYShare: { i: 1, j: 2, y: 'e0ae75077715686a121acb41b29a55bde426971154f40a41fc317f7f774a9424', v: 'f76ef629dfc15ab5e4531e532b5d67f2176637ca752b195876b7e3172459c969', u: 'fe6b89fb6acfcd7392c35c084f58bde0846b888c4df57e466caf0a3271b06a05', chaincode: '1c34e5dfbbd4a870f4479caaa5e6a46e3438f976ad5aefd4905b8fe8bca1101e', }, }; const validUserSignShare = { xShare: { i: 1, y: '4d9343988e68191aac945a6963031dddde3490f9020d0571a6e6c6e15cca0296', u: '1e159d6a0ae3a8dccc74615113e7c3e25d3080e5e0ffeb0ae04dd6a967268102', r: 'c8f64cc48926216c3f60e1d8ff1e24eba060d7c1ff020d0fc1d735d4564efd03', R: '9be2208ee28cd4b2577a9a66f6aab1ed8b08a300969eeb9b203a52aa54d2c23c', }, rShares: { 3: { i: 3, j: 1, u: 'd675f9099fbef03aa9fcdca4009286f435e56369c374d0042f03cc60b49e690a', v: '3c090e88ed42da0dd0bade35c8d6b88bc050284536b98e5b27d33ff45da9755b', r: '7f16224dbf5b02adb6c21380fcb2a8ee00323daae62cac3575a4d328fd23a905', R: '9be2208ee28cd4b2577a9a66f6aab1ed8b08a300969eeb9b203a52aa54d2c23c', commitment: '445c8cb1dee0166b6bdd5ad1d0a53fbfe86c4d3a470f184745530a863eedff28', }, }, }; const validBitgoToUserSignShare = { xShare: { i: 3, y: '4d9343988e68191aac945a6963031dddde3490f9020d0571a6e6c6e15cca0296', u: '1315dbe18069825b4a27188b813eae7ff2917a614499ed553e70d65d4fa4820b', r: 'd0539375e6566f2fe540cba48c5e56bd1cdf68cfe1f0d527d2b730fe4e879809', R: 'c883fe2ae9b8da1764cc36a526cfa1a21f81d604320b209867f8de9223f1de32', }, rShares: { 1: { i: 1, j: 3, u: '9ce3204a8c9757738967f3f81b463d87267bf6f2c0e5eaf2843167537b872b0b', v: '01ea3f425b1adf8aec6cfe4fc8f9b94755c34657965f32397655dcd784f1b517', r: '0375e8c5a5691a73c21df00d49d423e3f83fe08d7b5d5af33c5c6aa9cae59d0a', R: 'c883fe2ae9b8da1764cc36a526cfa1a21f81d604320b209867f8de9223f1de32', commitment: '62b21f98bf885841ad469145192d4df0697b3f42c581e3e926394eae0b101ecb', }, }, }; const txRequest = { txRequestId: 'randomId', unsignedTxs: [{ signableHex: 'MPC on a Friday night', serializedTxHex: 'MPC on a Friday night' }], signatureShares: [ { from: 'bitgo', to: 'user', share: validBitgoToUserSignShare.rShares[1].r + validBitgoToUserSignShare.rShares[1].R, }, ], }; beforeEach(function () { sandbox = sinon.createSandbox(); }); afterEach(function () { sandbox.restore(); }); before('initializes mpc', async function () { const hdTree = await sdk_core_1.Ed25519BIP32.initialize(); MPC = await sdk_core_1.Eddsa.initialize(hdTree); }); before(async function () { bitgoKeyShare = await MPC.keyShare(3, 2, 3); userGpgKey = await openpgp.generateKey({ userIDs: [ { name: 'test', email: 'test@test.com', }, ], curve: 'secp256k1', }); backupGpgKey = await openpgp.generateKey({ userIDs: [ { name: 'testBackup', email: 'testBackup@test.com', }, ], curve: 'secp256k1', }); bitgoGpgKey = await openpgp.generateKey({ userIDs: [ { name: 'bitgo', email: 'bitgo@test.com', }, ], curve: 'secp256k1', }); const constants = { mpc: { bitgoPublicKey: bitgoGpgKey.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(23).reply(200, { ttl: 3600, constants }); const walletData = { id: '5b34252f1bf349930e34020a00000000', coin: 'tsol', keys: [ '5b3424f91bf349930e34017500000000', '5b3424f91bf349930e34017600000000', '5b3424f91bf349930e34017700000000', ], coinSpecific: {}, multisigType: 'tss', }; wallet = new sdk_core_1.Wallet(bitgo, baseCoin, walletData); tssUtils = new sdk_core_1.TssUtils(bitgo, baseCoin, wallet); }); after(function () { nock.cleanAll(); }); describe('TSS key chains:', async function () { it('should generate TSS key chains', async function () { const userKeyShare = MPC.keyShare(1, 2, 3); const backupKeyShare = MPC.keyShare(2, 2, 3); const nockedBitGoKeychain = await nockBitgoKeychain({ coin: coinName, userKeyShare, backupKeyShare, bitgoKeyShare, userGpgKey, backupGpgKey, bitgoGpgKey, }); const nockedUserKeychain = await nockUserKeychain({ coin: coinName }); await nockBackupKeychain({ coin: coinName }); const bitgoKeychain = await tssUtils.createBitgoKeychain({ userGpgKey, backupGpgKey, userKeyShare, backupKeyShare, }); const userKeychain = await tssUtils.createUserKeychain({ userGpgKey, backupGpgKey, userKeyShare, backupKeyShare, bitgoKeychain, passphrase: 'passphrase', }); const backupKeychain = await tssUtils.createBackupKeychain({ userGpgKey, backupGpgKey, userKeyShare, backupKeyShare, bitgoKeychain, passphrase: 'passphrase', }); bitgoKeychain.should.deepEqual(nockedBitGoKeychain); userKeychain.should.deepEqual(nockedUserKeychain); // unencrypted `prv` property should exist on backup keychain JSON.stringify({ uShare: backupKeyShare.uShare, bitgoYShare: bitgoKeyShare.yShares[2], userYShare: userKeyShare.yShares[2], }).should.equal(backupKeychain.prv); should.exist(backupKeychain.encryptedPrv); }); it('should generate TSS key chains without passphrase', async function () { const userKeyShare = MPC.keyShare(1, 2, 3); const backupKeyShare = MPC.keyShare(2, 2, 3); const nockedBitGoKeychain = await nockBitgoKeychain({ coin: coinName, userKeyShare, backupKeyShare, bitgoKeyShare, userGpgKey, // reusing the user gpg key as the backup gpg key, i.e. the user is their own the backup provider backupGpgKey, bitgoGpgKey, }); const nockedUserKeychain = await nockUserKeychain({ coin: coinName }); await nockBackupKeychain({ coin: coinName }); const bitgoKeychain = await tssUtils.createBitgoKeychain({ userGpgKey, backupGpgKey, userKeyShare, backupKeyShare, }); const userKeychain = await tssUtils.createUserKeychain({ userGpgKey, backupGpgKey, userKeyShare, backupKeyShare, bitgoKeychain, }); const backupKeychain = await tssUtils.createBackupKeychain({ userGpgKey, backupGpgKey, userKeyShare, backupKeyShare, bitgoKeychain, }); bitgoKeychain.should.deepEqual(nockedBitGoKeychain); userKeychain.should.deepEqual(nockedUserKeychain); // unencrypted `prv` property should exist on backup keychain JSON.stringify({ uShare: backupKeyShare.uShare, bitgoYShare: bitgoKeyShare.yShares[2], userYShare: userKeyShare.yShares[2], }).should.equal(backupKeychain.prv); }); it('should generate TSS key chains with optional params', async function () { const enterprise = 'enterprise'; const userKeyShare = MPC.keyShare(1, 2, 3); const backupKeyShare = MPC.keyShare(2, 2, 3); const nockedBitGoKeychain = await nockBitgoKeychain({ coin: coinName, userKeyShare, backupKeyShare, bitgoKeyShare, userGpgKey, backupGpgKey, bitgoGpgKey, }); const nockedUserKeychain = await nockUserKeychain({ coin: coinName }); await nockBackupKeychain({ coin: coinName }); const bitgoKeychain = await tssUtils.createBitgoKeychain({ userGpgKey, backupGpgKey, userKeyShare, backupKeyShare, enterprise, }); const userKeychain = await tssUtils.createUserKeychain({ userGpgKey, backupGpgKey, userKeyShare, backupKeyShare, bitgoKeychain, passphrase: 'passphrase', originalPasscodeEncryptionCode: 'originalPasscodeEncryptionCode', }); const backupKeychain = await tssUtils.createBackupKeychain({ userGpgKey, backupGpgKey, userKeyShare, backupKeyShare, bitgoKeychain, passphrase: 'passphrase', }); bitgoKeychain.should.deepEqual(nockedBitGoKeychain); userKeychain.should.deepEqual(nockedUserKeychain); // unencrypted `prv` property should exist on backup keychain JSON.stringify({ uShare: backupKeyShare.uShare, bitgoYShare: bitgoKeyShare.yShares[2], userYShare: userKeyShare.yShares[2], }).should.equal(backupKeychain.prv); should.exist(backupKeychain.encryptedPrv); }); it('should fail to generate TSS keychains when received invalid number of wallet signatures', async function () { const userKeyShare = MPC.keyShare(1, 2, 3); const backupKeyShare = MPC.keyShare(2, 2, 3); const bitgoKeychain = await generateBitgoKeychain({ coin: coinName, userKeyShare, backupKeyShare, bitgoKeyShare, userGpgKey, backupGpgKey, bitgoGpgKey, }); const certsString = await (0, sdk_core_1.createSharedDataProof)(bitgoGpgKey.privateKey, userGpgKey.publicKey, []); const certsKey = await openpgp.readKey({ armoredKey: certsString }); const finalKey = new openpgp.PacketList(); certsKey.toPacketList().forEach((packet) => finalKey.push(packet)); // the underlying function only requires two arguments but the according .d.ts file for openpgp has the further // arguments marked as mandatory as well. // 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(userGpgKey.publicKey, backupGpgKey.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 userKeyShare = MPC.keyShare(1, 2, 3); const backupKeyShare = MPC.keyShare(2, 2, 3); const bitgoKeychain = await generateBitgoKeychain({ coin: coinName, userKeyShare, backupKeyShare, bitgoKeyShare, userGpgKey, backupGpgKey, bitgoGpgKey, }); // 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(backupGpgKey.publicKey, backupGpgKey.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`); }); it('should fail to generate TSS keychains when wallet signature is for different key share', async function () { const userKeyShare = MPC.keyShare(1, 2, 3); const backupKeyShare = MPC.keyShare(2, 2, 3); const customBitgoKeyShare = MPC.keyShare(3, 2, 3); const bitgoKeychain1 = await generateBitgoKeychain({ coin: coinName, userKeyShare, backupKeyShare, bitgoKeyShare, userGpgKey, backupGpgKey, bitgoGpgKey, }); const bitgoKeychain2 = await generateBitgoKeychain({ coin: coinName, userKeyShare, backupKeyShare, bitgoKeyShare: customBitgoKeyShare, userGpgKey, backupGpgKey, bitgoGpgKey, }); // using the other bitgo keychains common keychain and walletHSMGPGPublicKeySigs so that the verification of the // commmon keychain passes but fails for the bitgo to user/ backup shares bitgoKeychain1.commonKeychain = bitgoKeychain2.commonKeychain; bitgoKeychain1.walletHSMGPGPublicKeySigs = bitgoKeychain2.walletHSMGPGPublicKeySigs; await tssUtils .createUserKeychain({ userGpgKey, backupGpgKey, userKeyShare, backupKeyShare, bitgoKeychain: bitgoKeychain1, }) .should.be.rejectedWith('bitgo share mismatch'); await tssUtils .createBackupKeychain({ userGpgKey, backupGpgKey, userKeyShare, backupKeyShare, bitgoKeychain: bitgoKeychain1, }) .should.be.rejectedWith('bitgo share mismatch'); }); it('should fail to generate TSS key chains when common keychains do not match', async function () { const userKeyShare = MPC.keyShare(1, 2, 3); const backupKeyShare = MPC.keyShare(2, 2, 3); const nockedBitGoKeychain = await nockBitgoKeychain({ coin: coinName, userKeyShare, backupKeyShare, bitgoKeyShare, userGpgKey, backupGpgKey, bitgoGpgKey, }); const bitgoKeychain = await tssUtils.createBitgoKeychain({ userGpgKey, backupGpgKey, userKeyShare, backupKeyShare, }); bitgoKeychain.should.deepEqual(nockedBitGoKeychain); await tssUtils .createUserKeychain({ userGpgKey, backupGpgKey, userKeyShare, backupKeyShare: MPC.keyShare(2, 2, 3), bitgoKeychain, passphrase: 'passphrase', }) .should.be.rejectedWith('Failed to create user keychain - commonKeychains do not match.'); await tssUtils .createUserKeychain({ userGpgKey, backupGpgKey, userKeyShare: MPC.keyShare(1, 2, 3), backupKeyShare, bitgoKeychain, passphrase: 'passphrase', }) .should.be.rejectedWith('Failed to create user keychain - commonKeychains do not match.'); await tssUtils .createBackupKeychain({ userGpgKey, backupGpgKey, userKeyShare: MPC.keyShare(1, 2, 3), backupKeyShare, bitgoKeychain, passphrase: 'passphrase', }) .should.be.rejectedWith('Failed to create backup keychain - commonKeychains do not match.'); await tssUtils .createBackupKeychain({ userGpgKey, backupGpgKey, userKeyShare, backupKeyShare: MPC.keyShare(2, 2, 3), bitgoKeychain, passphrase: 'passphrase', }) .should.be.rejectedWith('Failed to create backup keychain - commonKeychains do not match.'); }); }); describe('signTxRequest:', function () { const txRequestId = 'randomid'; const txRequest = { txRequestId, transactions: [], unsignedTxs: [ { serializedTxHex: 'MPC on a Friday night', signableHex: 'MPC on a Friday night', derivationPath: 'm/0', }, ], date: new Date().toISOString(), intent: { intentType: 'payment', }, latest: true, state: 'pendingUserSignature', walletType: 'hot', walletId: 'walletId', policiesChecked: true, version: 1, userId: 'userId', }; beforeEach(async function () { const userSignShare = validUserSignShare; const rShare = userSignShare.rShares[3]; const signatureShare = { from: sdk_core_1.SignatureShareType.USER, to: sdk_core_1.SignatureShareType.BITGO, share: rShare.r + rShare.R, }; await (0, common_1.nockSendSignatureShare)({ walletId: wallet.id(), txRequestId: txRequest.txRequestId, signatureShare, }); const signatureShare2 = { from: sdk_core_1.SignatureShareType.BITGO, to: sdk_core_1.SignatureShareType.USER, share: validBitgoToUserSignShare.rShares[1].r + validBitgoToUserSignShare.rShares[1].R, }; const response = { txRequests: [{ ...txRequest, signatureShares: [signatureShare2] }] }; await (0, common_1.nockGetTxRequest)({ walletId: wallet.id(), txRequestId: txRequest.txRequestId, response }); const bitgoToUserCommitmentShare = { from: sdk_core_1.SignatureShareType.BITGO, to: sdk_core_1.SignatureShareType.USER, type: sdk_core_1.CommitmentType.COMMITMENT, share: validBitgoToUserSignShare.rShares[1].commitment, }; const exchangeCommitResponse = { commitmentShare: bitgoToUserCommitmentShare }; await (0, common_1.nockExchangeCommitments)({ walletId: wallet.id(), txRequestId: txRequest.txRequestId, response: exchangeCommitResponse, }); }); it('signTxRequest should succeed with txRequest object as input', async function () { const signedTxRequest = await tssUtils.signTxRequest({ txRequest, prv: JSON.stringify(validUserSigningMaterial), reqId, }); signedTxRequest.unsignedTxs.should.deepEqual(txRequest.unsignedTxs); sandbox.verifyAndRestore(); }); it('signTxRequest should succeed with txRequest id as input', async function () { const getTxRequest = sandbox.stub(tssUtils, 'getTxRequest'); getTxRequest.resolves(txRequest); getTxRequest.calledWith(txRequestId); const signedTxRequest = await tssUtils.signTxRequest({ txRequest: txRequestId, prv: JSON.stringify(validUserSigningMaterial), reqId, }); signedTxRequest.unsignedTxs.should.deepEqual(txRequest.unsignedTxs); sandbox.verifyAndRestore(); }); }); describe('signTxRequest With Commitment:', function () { const txRequestId = 'randomid'; const txRequest = { txRequestId, transactions: [], unsignedTxs: [ { serializedTxHex: 'MPC on a Friday night', signableHex: 'MPC on a Friday night', derivationPath: 'm/0', }, ], date: new Date().toISOString(), intent: { intentType: 'payment', }, latest: true, state: 'pendingUserSignature', walletType: 'hot', walletId: 'walletId', policiesChecked: true, version: 1, userId: 'userId', }; beforeEach(async function () { const userSignShare = validUserSignShare; const rShare = userSignShare.rShares[3]; const signatureShare = { from: sdk_core_1.SignatureShareType.USER, to: sdk_core_1.SignatureShareType.BITGO, share: rShare.r + rShare.R, }; await (0, common_1.nockSendSignatureShare)({ walletId: wallet.id(), txRequestId: txRequest.txRequestId, signatureShare, }); const signatureShare2 = { from: sdk_core_1.SignatureShareType.BITGO, to: sdk_core_1.SignatureShareType.USER, share: validBitgoToUserSignShare.rShares[1].r + validBitgoToUserSignShare.rShares[1].R, }; const response = { txRequests: [{ ...txRequest, signatureShares: [signatureShare2] }] }; await (0, common_1.nockGetTxRequest)({ walletId: wallet.id(), txRequestId: txRequest.txRequestId, response }); const bitgoToUserCommitmentShare = { from: sdk_core_1.SignatureShareType.BITGO, to: sdk_core_1.SignatureShareType.USER, type: sdk_core_1.CommitmentType.COMMITMENT, share: validBitgoToUserSignShare.rShares[1].commitment, }; const exchangeCommitResponse = { commitmentShare: bitgoToUserCommitmentShare }; await (0, common_1.nockExchangeCommitments)({ walletId: wallet.id(), txRequestId: txRequest.txRequestId, response: exchangeCommitResponse, }); }); it('signTxRequest should succeed with txRequest object as input', async function () { const signedTxRequest = await tssUtils.signTxRequest({ txRequest, prv: JSON.stringify(validUserSigningMaterial), reqId, }); signedTxRequest.unsignedTxs.should.deepEqual(txRequest.unsignedTxs); sandbox.verifyAndRestore(); }); it('signTxRequest should succeed with txRequest id as input', async function () { const getTxRequest = sandbox.stub(tssUtils, 'getTxRequest'); getTxRequest.resolves(txRequest); getTxRequest.calledWith(txRequestId); const signedTxRequest = await tssUtils.signTxRequest({ txRequest: txRequestId, prv: JSON.stringify(validUserSigningMaterial), reqId, }); signedTxRequest.unsignedTxs.should.deepEqual(txRequest.unsignedTxs); sandbox.verifyAndRestore(); }); }); describe('signTxRequestForMessage:', function () { const txRequestId = 'randomid-abc'; const messageRaw = 'hello world'; const messageEncoded = Buffer.from(`${messageRaw}`).toString('hex'); const bufferToSign = Buffer.from(messageEncoded, 'hex'); const txRequest = { txRequestId, transactions: [], messages: [ { state: 'pendingSignature', signatureShares: [], messageRaw, messageEncoded, derivationPath: 'm/0', }, ], unsignedTxs: [], date: new Date().toISOString(), intent: { intentType: 'payment', }, latest: true, state: 'pendingUserSignature', walletType: 'hot', walletId: 'walletId', policiesChecked: true, version: 1, userId: 'userId', apiVersion: 'full', }; beforeEach(async function () { const rShare = validUserSignShare.rShares[3]; const signatureShare = { from: sdk_core_1.SignatureShareType.USER, to: sdk_core_1.SignatureShareType.BITGO, share: rShare.r + rShare.R, }; await (0, common_1.nockSendSignatureShare)({ walletId: wallet.id(), txRequestId: txRequest.txRequestId, signatureShare, requestType: sdk_core_1.RequestType.message, apiMode: 'full', }); const signatureShare2 = { from: sdk_core_1.SignatureShareType.BITGO, to: sdk_core_1.SignatureShareType.USER, share: validBitgoToUserSignShare.rShares[1].r + validBitgoToUserSignShare.rShares[1].R, }; txRequest.messages[0].signatureShares.push(signatureShare2); const response = { txRequests: [{ ...txRequest, apiVersion: 'full' }] }; await (0, common_1.nockGetTxRequest)({ walletId: wallet.id(), txRequestId: txRequest.txRequestId, response }); const bitgoToUserCommitmentShare = { from: sdk_core_1.SignatureShareType.BITGO, to: sdk_core_1.SignatureShareType.USER, type: sdk_core_1.CommitmentType.COMMITMENT, share: validBitgoToUserSignShare.rShares[1].commitment, }; const exchangeCommitResponse = { commitmentShare: bitgoToUserCommitmentShare }; await (0, common_1.nockExchangeCommitments)({ walletId: wallet.id(), txRequestId: txRequest.txRequestId, response: exchangeCommitResponse, apiMode: 'full', }); }); afterEach(async function () { txRequest.messages[0].signatureShares = []; }); it('signTxRequest should succeed with txRequest object as input', async function () { const signedTxRequest = await tssUtils.signTxRequestForMessage({ messageRaw, bufferToSign, txRequest, prv: JSON.stringify(validUserSigningMaterial), reqId, }); signedTxRequest.messages.should.deepEqual(txRequest.messages); sandbox.verifyAndRestore(); }); it('signTxRequest should succeed with txRequest id as input', async function () { const getTxRequest = sandbox.stub(tssUtils, 'getTxRequest'); getTxRequest.resolves(txRequest); getTxRequest.calledWith(txRequestId); const signedTxRequest = await tssUtils.signTxRequestForMessage({ txRequest: txRequestId, prv: JSON.stringify(validUserSigningMaterial), reqId, messageRaw, bufferToSign, }); signedTxRequest.messages.should.deepEqual(txRequest.messages); sandbox.verifyAndRestore(); }); }); describe('prebuildTxWithIntent:', async function () { it('should build single recipient tx', async function () { const nockedCreateTx = await (0, common_1.nockCreateTxRequest)({ walletId: wallet.id(), requestBody: { apiVersion: 'lite', intent: { intentType: 'payment', recipients: [ { address: { address: 'recipient', }, amount: { value: '10000', symbol: 'tsol', }, }, ], }, }, // don't care about the actual response - just need to make sure request body matches response: {}, }); await tssUtils.prebuildTxWithIntent({ reqId, recipients: [ { address: 'recipient', amount: '10000', }, ], intentType: 'payment', }); nockedCreateTx.isDone().should.be.true(); }); it('should build multiple recipients with memo tx', async function () { const nockedCreateTx = await (0, common_1.nockCreateTxRequest)({ walletId: wallet.id(), requestBody: { apiVersion: 'lite', intent: { intentType: 'payment', recipients: [ { address: { address: 'recipient1', }, amount: { value: '10000', symbol: 'tsol', }, }, { address: { address: 'recipient2', }, amount: { value: '20000', symbol: 'tsol', }, }, ], memo: 'memo', }, }, // don't care about the actual response - just need to make sure request body matches response: {}, }); await tssUtils.prebuildTxWithIntent({ reqId, recipients: [ { address: 'recipient1', amount: '10000', }, { address: 'recipient2', amount: '20000', }, ], memo: { value: 'memo', type: 'text', }, intentType: 'payment', }); nockedCreateTx.isDone().should.be.true(); }); }); describe('delete SignatureShare:', async function () { it('should succeed to delete Signature Share', async function () { const signatureShare = { from: 'user', to: 'bitgo', share: '128bytestring' }; const nock = await (0, common_1.nockDeleteSignatureShare)({ walletId: wallet.id(), txRequestId: txRequest.txRequestId, signatureShare, }); const response = await tssUtils.deleteSignatureShares(txRequest.txRequestId); response.should.deepEqual([signatureShare]); response.should.length(1); nock.isDone().should.equal(true); }); it('should call setRequestTracer', async function () { const signatureShare = { from: 'user', to: 'bitgo', share: '128bytestring' }; const nock = await (0, common_1.nockDeleteSignatureShare)({ walletId: wallet.id(), txRequestId: txRequest.txRequestId, signatureShare, }); const reqId = new sdk_core_1.RequestTracer(); const setRequestTracerSpy = sinon.spy(bitgo, 'setRequestTracer'); setRequestTracerSpy.withArgs(reqId); const response = await tssUtils.deleteSignatureShares(txRequest.txRequestId, reqId); response.should.deepEqual([signatureShare]); response.should.length(1); nock.isDone().should.equal(true); sinon.assert.calledOnce(setRequestTracerSpy); setRequestTracerSpy.restore(); }); }); describe('sendTxRequest:', async function () { it('should succeed to send tx request', async function () { const nock = await (0, common_1.nockSendTxRequest)({ coin: coinName, walletId: wallet.id(), txRequestId: txRequest.txRequestId, }); await tssUtils.sendTxRequest(txRequest.txRequestId).should.be.fulfilled(); nock.isDone().should.equal(true); }); it('should call setRequestTracer', async function () { const nock = await (0, common_1.nockSendTxRequest)({ coin: coinName, walletId: wallet.id(), txRequestId: txRequest.txRequestId, }); const reqId = new sdk_core_1.RequestTracer(); const setRequestTracerSpy = sinon.spy(bitgo, 'setRequestTracer'); setRequestTracerSpy.withArgs(reqId); await tssUtils.sendTxRequest(txRequest.txRequestId, reqId).should.be.fulfilled(); nock.isDone().should.equal(true); sinon.assert.calledOnce(setRequestTracerSpy); setRequestTracerSpy.restore(); }); }); describe('createUserToBitgoCommitmentShare', function () { it('should create a valid commitmentShare', async function () { const value = 'randomstring'; const validUserToBitgoCommitmentShare = { from: sdk_core_1.SignatureShareType.USER, to: sdk_core_1.SignatureShareType.BITGO, type: sdk_core_1.CommitmentType.COMMITMENT, share: value, }; const commitmentShare = tssUtils.createUserToBitgoCommitmentShare(value); commitmentShare.should.deepEqual(validUserToBitgoCommitmentShare); }); }); describe('createUserToBitgoEncryptedSignerShare', function () { it('should create a valid encryptedSignerShare', async function () { const value = 'randomstring'; const validUserToBitgoEncryptedSignerShare = { from: sdk_core_1.SignatureShareType.USER, to: sdk_core_1.SignatureShareType.BITGO, type: sdk_core_1.EncryptedSignerShareType.ENCRYPTED_SIGNER_SHARE, share: value, }; const encryptedSignerShare = tssUtils.createUserToBitgoEncryptedSignerShare(value); encryptedSignerShare.should.deepEqual(validUserToBitgoEncryptedSignerShare); }); }); describe('supportedTxRequestVersions', function () { it('should return full for custodial wallets', async function () { const custodialWallet = new sdk_core_1.Wallet(bitgo, baseCoin, { multisigType: 'tss', type: 'custodial' }); const custodialTssUtils = new sdk_core_1.TssUtils(bitgo, baseCoin, custodialWallet); custodialTssUtils.supportedTxRequestVersions().should.deepEqual(['full']); }); it('should return full for cold wallets', async function () { const coldWallet = new sdk_core_1.Wallet(bitgo, baseCoin, { multisigType: 'tss', type: 'cold' }); const coldWalletTssUtils = new sdk_core_1.TssUtils(bitgo, baseCoin, coldWallet); coldWalletTssUtils.supportedTxRequestVersions().should.deepEqual(['full']); }); it('should return full and lite for hot wallets', async function () { const hotWallet = new sdk_core_1.Wallet(bitgo, baseCoin, { multisigType: 'tss', type: 'hot' }); const hotTssUtils = new sdk_core_1.TssUtils(bitgo, baseCoin, hotWallet); const supportedTxRequestVersions = hotTssUtils.supportedTxRequestVersions(); supportedTxRequestVersions.should.deepEqual(['lite', 'full']); }); it('should return empty for trading wallets', function () { const tradingWallets = new sdk_core_1.Wallet(bitgo, baseCoin, { multisigType: 'tss', type: 'trading' }); const tradingWalletTssUtils = new sdk_core_1.TssUtils(bitgo, baseCoin, tradingWallets); const supportedTxRequestVersions = tradingWalletTssUtils.supportedTxRequestVersions(); supportedTxRequestVersions.should.deepEqual([]); }); it('should return empty for non-tss wallets', function () { const nonTssWalletData = { coin: 'btc', multisigType: 'onchain' }; const btcCoin = bitgo.coin('tbtc'); const nonTssWallet = new sdk_core_1.Wallet(bitgo, btcCoin, nonTssWalletData); const nonTssWalletTssUtils = new sdk_core_1.TssUtils(bitgo, btcCoin, nonTssWallet); nonTssWalletTssUtils.supportedTxRequestVersions().should.deepEqual([]); }); }); describe('isPendingApprovalTxRequestFull', () => { it('should return true for full apiVersion and pendingApproval state', async () => { const txRequest = { apiVersion: 'full', state: 'pendingApproval', }; const result = await tssUtils.isPendingApprovalTxRequestFull(txRequest); result.should.be.true(); }); it('should return false for non-full apiVersion', async () => { const txRequest = { apiVersion: 'lite', state: 'pendingApproval', }; const result = await tssUtils.isPendingApprovalTxRequestFull(txRequest); result.should.be.false(); }); it('should return false for non-pendingApproval state', async () => { const txRequest = { apiVersion: 'full', state: 'pendingDelivery', }; const result = await tssUtils.isPendingApprovalTxRequestFull(txRequest); result.should.be.false(); }); }); // #region Nock helpers async function generateBitgoKeychain(params) { const bitgoCombined = MPC.keyCombine(params.bitgoKeyShare.uShare, [ params.userKeyShare.yShares[3], params.backupKeyShare.yShares[3], ]); const userGpgKeyActual = await openpgp.readKey({ armoredKey: params.userGpgKey.publicKey }); const backupGpgKeyActual = await openpgp.readKey({ armoredKey: params.backupGpgKey.publicKey }); const bitgoToUserMessage = await openpgp.createMessage({ text: Buffer.concat([ Buffer.from(params.bitgoKeyShare.yShares[1].u, 'hex'), Buffer.from(params.bitgoKeyShare.yShares[1].chaincode, 'hex'), ]).toString('hex'), }); const encryptedBitgoToUserMessage = await openpgp.encrypt({ message: bitgoToUserMessage, encryptionKeys: [userGpgKeyActual.toPublic()], format: 'armored', }); const bitgoToBackupMessage = await openpgp.createMessage({ text: Buffer.concat([ Buffer.from(params.bitgoKeyShare.yShares[2].u, 'hex'), Buffer.from(params.bitgoKeyShare.yShares[2].chaincode, 'hex'), ]).toString('hex'), }); const encryptedBitgoToBackupMessage = await openpgp.encrypt({ message: bitgoToBackupMessage, encryptionKeys: [backupGpgKeyActual.toPublic()], format: 'armored', }); const bitgoKeychain = { id: '3', pub: '', commonKeychain: bitgoCombined.pShare.y + bitgoCombined.pShare.chaincode, keyShares: [ { from: 'bitgo', to: 'user', publicShare: params.bitgoKeyShare.yShares[1].y + params.bitgoKeyShare.yShares[1].chaincode, privateShare: encryptedBitgoToUserMessage.toString(), vssProof: params.bitgoKeyShare.yShares[1].v, }, { from: 'bitgo', to: 'backup', publicShare: params.bitgoKeyShare.yShares[2].y + params.bitgoKeyShare.yShares[2].chaincode, privateShare: encryptedBitgoToBackupMessage.toString(), vssProof: params.bitgoKeyShare.yShares[2].v, }, ], type: 'tss', }; const userKeyId = userGpgKeyActual.keyPacket.getFingerprint(); const backupKeyId = backupGpgKeyActual.keyPacket.getFingerprint(); const bitgoToUserPublicShare = Buffer.from(await sodium.crypto_scalarmult_ed25519_base_noclamp(Buffer.from(params.bitgoKeyShare.yShares[1].u, 'hex'))).toString('hex') + params.bitgoKeyShare.yShares[1].chaincode; const bitgoToBackupPublicShare = Buffer.from(await sodium.crypto_scalarmult_ed25519_base_noclamp(Buffer.from(params.bitgoKeyShare.yShares[2].u, 'hex'))).toString('hex') + params.bitgoKeyShare.yShares[2].chaincode; bitgoKeychain.walletHSMGPGPublicKeySigs = await (0, helpers_1.createWalletSignatures)(params.bitgoGpgKey.privateKey, params.userGpgKey.publicKey, params.backupGpgKey.publicKey, [ { name: 'commonKeychain', value: bitgoCombined.pShare.y + bitgoCombined.pShare.chaincode }, { name: 'userKeyId', value: userKeyId }, { name: 'backupKeyId', value: backupKeyId }, { name: 'bitgoToUserPublicShare', value: bitgoToUserPublicShare }, { name: 'bitgoToBackupPublicShare', value: bitgoToBackupPublicShare }, ]); return bitgoKeychain; } async function nockBitgoKeychain(params) { const bitgoKeychain = await generateBitgoKeychain(params); nock(bgUrl) .post(`/api/v2/${params.coin}/key`, _.matches({ keyType: 'tss', source: 'bitgo' })) .reply(200, bitgoKeychain); return bitgoKeychain; } async function nockUserKeychain(params) { const userKeychain = { id: '1', pub: '', type: 'tss', }; nock('https://bitgo.fakeurl') .post(`/api/v2/${params.coin}/key`, _.matches({ keyType: 'tss', source: 'user' })) .reply(200, userKeychain); return userKeychain; } async function nockBackupKeychain(params) { const backupKeychain = { id: '2', pub: '', type: 'tss', }; nock('https://bitgo.fakeurl') .post(`/api/v2/${params.coin}/key`, _.matches({ keyType: 'tss', source: 'backup' })) .reply(200, backupKeychain); return backupKeychain; } describe('getPublicKeyFromCommonKeychain', function () { // 32-byte ed25519 public key as hex (64 chars) — the format produced by DKG getSharePublicKey().toString('hex') const mpcv2CommonKeychain = 'a304733c16cc821fe171d5c7dbd7276fd90deae808b7553d17a1e55e4a76b270'; // MPCv1 appends a 32-byte chaincode after the public key const chaincode = '9d91c2e6353202cf61f8f275158b3468e9a00f7872fc2fd310b72cd026e2e2f9'; const mpcv1CommonKeychain = mpcv2CommonKeychain + chaincode; it('should decode to the same 32-byte public key for both MPCv1 (128 chars) and MPCv2 (64 chars)', function () { mpcv1CommonKeychain.length.should.equal(128); mpcv2CommonKeychain.length.should.equal(64); const v1Result = sdk_core_1.TssUtils.getPublicKeyFromCommonKeychain(mpcv1CommonKeychain); const v2Result = sdk_core_1.TssUtils.getPublicKeyFromCommonKeychain(mpcv2CommonKeychain); v1Result.should.equal(v2Result); v1Result.should.equal('ByMPeVxs7e8zGecu8n1M43Mq9qkxBSypNNwHeEu2N6vb'); }); it('should throw for an invalid commonKeychain length', function () { should.throws(() => sdk_core_1.TssUtils.getPublicKeyFromCommonKeychain('abcd'), /Invalid commonKeychain length, expected 64 \(MPCv2\) or 128 \(MPCv1\), got 4/); }); }); describe('getBitgoGpgPubkeyBasedOnFeatureFlags', function () { it('should return eddsaMpcv2PublicKey when present in feature flags response', async function () { const ed25519KeyPair = await openpgp.generateKey({ userIDs: [{ name: 'bitgo eddsa', email: 'bitgo@test.com' }], curve: 'ed25519', }); const response = { name: 'irrelevant', publicKey: ed25519KeyPair.publicKey, mpcv2PublicKey: ed25519KeyPair.publicKey, eddsaMpcv2PublicKey: ed25519KeyPair.publicKey, enterpriseId: 'enterprise_id', }; nock(bgUrl).get(`/api/v2/${coinName}/tss/pubkey`).query({ enterpriseId: 'enterprise_id' }).reply(200, response); const { eddsaMpcv2PublicKey } = await tssUtils.getBitgoGpgPubkeyBasedOnFeatureFlags('enterprise_id'); should.exist(eddsaMpcv2PublicKey); should.equal(ed25519KeyPair.publicKey, eddsaMpcv2PublicKey.armor()); }); it('should return undefined eddsaMpcv2PublicKey when absent from feature flags response', async function () { const keyPair = await openpgp.generateKey({ userIDs: [{ name: 'bitgo', email: 'bitgo@test.com' }], }); const response = { name: 'irrelevant', publicKey: keyPair.publicKey, mpcv2PublicKey: keyPair.publicKey, enterpriseId: 'enterprise_id', }; nock(bgUrl).get(`/api/v2/${coinName}/tss/pubkey`).query({ enterpriseId: 'enterprise_id' }).reply(200, response); const { eddsaMpcv2PublicKey } = await tssUtils.getBitgoGpgPubkeyBasedOnFeatureFlags('enterprise_id'); should.not.exist(eddsaMpcv2PublicKey); }); }); // #endregion Nock helpers }); //# sourceMappingURL=data:application/json;base64,ey