bitgo
Version:
BitGo JavaScript SDK
1,100 lines • 155 kB
JavaScript
"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