Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
245 changes: 237 additions & 8 deletions modules/bitgo/test/v2/unit/internal/tssUtils/eddsa.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
Eddsa,
EncryptedSignerShareType,
ExchangeCommitmentResponse,
InvalidTransactionError,
Keychain,
KeyShare,
RequestTracer,
Expand Down Expand Up @@ -127,9 +128,14 @@ describe('TSS Utils:', async function () {
},
};

// Sol TSS unsigned tx bytes — same fixtures as eddsaMPCv2/signTxRequest.ts
const solTssSignableHex =
'02010206c2d5b5f4fb9a9bcd8a2f303e4d06f78d8ded300713f456da2abff0b3ea0185aa051a34bc8acd438763976f96876115050f73828553566d111d7ac8bffebf587c4f5f5987bfe26aa66013efd96d36360f2b4336c91f993259fb56051305614d42f2ea13f8ff9d7958dbf269c6e36bfdf5cb5c43de4b4e1d3efb7dab3d5d028604000000000000000000000000000000000000000000000000000000000000000006a7d517192c568ee08a845f73d29788cf035c3145b21ab344d8062ea94000003a621f6d1cc4b8fb2a739aa08e4034da0fc588ece3bd857630de30f7edde45dd0204030205010404000000040200030c02000000f0a29a3b00000000';
const solTssSerializedTxHex = `02000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003bc9df0b397bec2ed3b6444a8c33f38267cc08b5fb2a7d37e26b6c487e26d15b7c07830eb78e26a88db5de4aa6986a327f09aed8c01533e5b972748ddf60b80f${solTssSignableHex}`;

const txRequest = {
txRequestId: 'randomId',
unsignedTxs: [{ signableHex: 'MPC on a Friday night', serializedTxHex: 'MPC on a Friday night' }],
unsignedTxs: [{ signableHex: solTssSignableHex, serializedTxHex: solTssSerializedTxHex }],
signatureShares: [
{
from: 'bitgo',
Expand Down Expand Up @@ -656,13 +662,21 @@ describe('TSS Utils:', async function () {
txRequestId: 'v2-signing-test',
unsignedTxs: [
{
serializedTxHex: 'test-payload',
signableHex: 'deadbeef',
serializedTxHex: solTssSerializedTxHex,
signableHex: solTssSignableHex,
derivationPath: 'm/0',
},
],
date: new Date().toISOString(),
intent: { intentType: 'payment' },
intent: {
intentType: 'payment',
recipients: [
{
address: { address: 'HMEgbR4S2hLKfst2VZUVpHVUu4FioFPyW5iUuJvZdMvs' },
amount: { value: '999990000', symbol: 'tsol' },
},
],
},
latest: true,
state: 'pendingUserSignature',
walletType: 'hot',
Expand All @@ -672,6 +686,10 @@ describe('TSS Utils:', async function () {
userId: 'userId',
};

beforeEach(function () {
sandbox.stub(baseCoin, 'verifyTransaction').resolves(true);
});

it('v2 R-share round-trip: encrypt via commitment, verify envelope, decrypt via createRShare', async function () {
const passphrase = 'test-passphrase';
const prv = JSON.stringify(validUserSigningMaterial);
Expand Down Expand Up @@ -716,14 +734,20 @@ describe('TSS Utils:', async function () {
transactions: [],
unsignedTxs: [
{
serializedTxHex: 'MPC on a Friday night',
signableHex: 'MPC on a Friday night',
serializedTxHex: solTssSerializedTxHex,
signableHex: solTssSignableHex,
derivationPath: 'm/0',
},
],
date: new Date().toISOString(),
intent: {
intentType: 'payment',
recipients: [
{
address: { address: 'HMEgbR4S2hLKfst2VZUVpHVUu4FioFPyW5iUuJvZdMvs' },
amount: { value: '999990000', symbol: 'tsol' },
},
],
},
latest: true,
state: 'pendingUserSignature',
Expand All @@ -735,6 +759,8 @@ describe('TSS Utils:', async function () {
};

beforeEach(async function () {
sandbox.stub(baseCoin, 'verifyTransaction').resolves(true);

const userSignShare = validUserSignShare;
const rShare = userSignShare.rShares[3];
const signatureShare: SignatureShareRecord = {
Expand Down Expand Up @@ -805,14 +831,20 @@ describe('TSS Utils:', async function () {
transactions: [],
unsignedTxs: [
{
serializedTxHex: 'MPC on a Friday night',
signableHex: 'MPC on a Friday night',
serializedTxHex: solTssSerializedTxHex,
signableHex: solTssSignableHex,
derivationPath: 'm/0',
},
],
date: new Date().toISOString(),
intent: {
intentType: 'payment',
recipients: [
{
address: { address: 'HMEgbR4S2hLKfst2VZUVpHVUu4FioFPyW5iUuJvZdMvs' },
amount: { value: '999990000', symbol: 'tsol' },
},
],
},
latest: true,
state: 'pendingUserSignature',
Expand All @@ -824,6 +856,8 @@ describe('TSS Utils:', async function () {
};

beforeEach(async function () {
sandbox.stub(baseCoin, 'verifyTransaction').resolves(true);

const userSignShare = validUserSignShare;
const rShare = userSignShare.rShares[3];
const signatureShare: SignatureShareRecord = {
Expand Down Expand Up @@ -886,6 +920,201 @@ describe('TSS Utils:', async function () {
});
});

describe('signTxRequest resolveEffectiveTxParams guard:', function () {
const txRequestId = 'randomid-guard';
const baseTxRequest: TxRequest = {
txRequestId,
transactions: [],
unsignedTxs: [
{
serializedTxHex: solTssSerializedTxHex,
signableHex: solTssSignableHex,
derivationPath: 'm/0',
},
],
date: new Date().toISOString(),
intent: {
intentType: 'payment',
recipients: [
{
address: { address: 'HMEgbR4S2hLKfst2VZUVpHVUu4FioFPyW5iUuJvZdMvs' },
amount: { value: '999990000', symbol: 'tsol' },
},
],
},
latest: true,
state: 'pendingUserSignature',
walletType: 'hot',
walletId: 'walletId',
policiesChecked: true,
version: 1,
userId: 'userId',
};

it('throws InvalidTransactionError when txParams is absent and intent has no recipients', async function () {
const maliciousTxRequest: TxRequest = {
...baseTxRequest,
intent: { intentType: 'stakingAuthorize' },
};
await tssUtils
.signTxRequest({
txRequest: maliciousTxRequest,
prv: JSON.stringify(validUserSigningMaterial),
reqId,
})
.should.be.rejectedWith(InvalidTransactionError);
});

it('uses intent recipients when txParams is absent', async function () {
const verifyStub = sandbox.stub(baseCoin, 'verifyTransaction').resolves(true);

const userSignShare = validUserSignShare;
const rShare = userSignShare.rShares[3];
const signatureShare: SignatureShareRecord = {
from: SignatureShareType.USER,
to: SignatureShareType.BITGO,
share: rShare.r + rShare.R,
};
await nockSendSignatureShare({
walletId: wallet.id(),
txRequestId: baseTxRequest.txRequestId,
signatureShare,
});
const signatureShare2: SignatureShareRecord = {
from: SignatureShareType.BITGO,
to: SignatureShareType.USER,
share: validBitgoToUserSignShare.rShares[1].r + validBitgoToUserSignShare.rShares[1].R,
};
const response = { txRequests: [{ ...baseTxRequest, signatureShares: [signatureShare2] }] };
await nockGetTxRequest({ walletId: wallet.id(), txRequestId: baseTxRequest.txRequestId, response });
const bitgoToUserCommitmentShare: CommitmentShareRecord = {
from: SignatureShareType.BITGO,
to: SignatureShareType.USER,
type: CommitmentType.COMMITMENT,
share: validBitgoToUserSignShare.rShares[1].commitment,
};
await nockExchangeCommitments({
walletId: wallet.id(),
txRequestId: baseTxRequest.txRequestId,
response: { commitmentShare: bitgoToUserCommitmentShare },
});

await tssUtils.signTxRequest({
txRequest: baseTxRequest,
prv: JSON.stringify(validUserSigningMaterial),
reqId,
});

verifyStub.calledOnce.should.be.true();
const verifyArgs = verifyStub.firstCall.args[0];
verifyArgs.txParams.recipients?.[0].address.should.equal('HMEgbR4S2hLKfst2VZUVpHVUu4FioFPyW5iUuJvZdMvs');
});

it('does not throw for allowlisted no-recipient intentType (deactivate)', async function () {
const verifyStub = sandbox.stub(baseCoin, 'verifyTransaction').resolves(true);

const deactivateTxRequest: TxRequest = {
...baseTxRequest,
intent: { intentType: 'deactivate' },
};

const userSignShare = validUserSignShare;
const rShare = userSignShare.rShares[3];
const signatureShare: SignatureShareRecord = {
from: SignatureShareType.USER,
to: SignatureShareType.BITGO,
share: rShare.r + rShare.R,
};
await nockSendSignatureShare({
walletId: wallet.id(),
txRequestId: deactivateTxRequest.txRequestId,
signatureShare,
});
const signatureShare2: SignatureShareRecord = {
from: SignatureShareType.BITGO,
to: SignatureShareType.USER,
share: validBitgoToUserSignShare.rShares[1].r + validBitgoToUserSignShare.rShares[1].R,
};
const response = { txRequests: [{ ...deactivateTxRequest, signatureShares: [signatureShare2] }] };
await nockGetTxRequest({
walletId: wallet.id(),
txRequestId: deactivateTxRequest.txRequestId,
response,
});
const bitgoToUserCommitmentShare: CommitmentShareRecord = {
from: SignatureShareType.BITGO,
to: SignatureShareType.USER,
type: CommitmentType.COMMITMENT,
share: validBitgoToUserSignShare.rShares[1].commitment,
};
await nockExchangeCommitments({
walletId: wallet.id(),
txRequestId: deactivateTxRequest.txRequestId,
response: { commitmentShare: bitgoToUserCommitmentShare },
});

await tssUtils.signTxRequest({
txRequest: deactivateTxRequest,
prv: JSON.stringify(validUserSigningMaterial),
reqId,
});

verifyStub.calledOnce.should.be.true();
});
});

describe('signEddsaTssUsingExternalSigner resolveEffectiveTxParams guard:', function () {
const externalGuardTxRequest: TxRequest = {
txRequestId: 'randomid-external-guard',
transactions: [],
unsignedTxs: [
{
serializedTxHex: solTssSerializedTxHex,
signableHex: solTssSignableHex,
derivationPath: 'm/0',
},
],
date: new Date().toISOString(),
intent: {
intentType: 'payment',
recipients: [
{
address: { address: 'HMEgbR4S2hLKfst2VZUVpHVUu4FioFPyW5iUuJvZdMvs' },
amount: { value: '999990000', symbol: 'tsol' },
},
],
},
latest: true,
state: 'pendingUserSignature',
walletType: 'hot',
walletId: 'walletId',
policiesChecked: true,
version: 1,
userId: 'userId',
};

it('throws InvalidTransactionError before external signer callbacks when intent has no recipients', async function () {
const commitmentGen = sandbox.stub().rejects(new Error('should not run'));
const maliciousTxRequest: TxRequest = {
...externalGuardTxRequest,
intent: { intentType: 'stakingAuthorize' },
};
await tssUtils
.signEddsaTssUsingExternalSigner(
maliciousTxRequest,
commitmentGen,
async function () {
throw new Error('should not run');
},
async function () {
throw new Error('should not run');
}
)
.should.be.rejectedWith(InvalidTransactionError);
commitmentGen.notCalled.should.be.true();
});
});

describe('signTxRequestForMessage:', function () {
const txRequestId = 'randomid-abc';
const messageRaw = 'hello world';
Expand Down
23 changes: 22 additions & 1 deletion modules/bitgo/test/v2/unit/signTransactionVerification.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,14 @@ import 'should';
import { BitGoAPI } from '@bitgo/sdk-api';
import { TestBitGo } from '@bitgo/sdk-test';
import { Tbtc } from '@bitgo/sdk-coin-btc';
import { common, BaseCoin, BitGoBase, Wallet, WalletSignTransactionOptions } from '@bitgo/sdk-core';
import {
common,
BaseCoin,
BitGoBase,
InvalidTransactionError,
Wallet,
WalletSignTransactionOptions,
} from '@bitgo/sdk-core';

describe('Wallet signTransaction with verifyTxParams', function () {
let wallet: Wallet;
Expand Down Expand Up @@ -151,4 +158,18 @@ describe('Wallet signTransaction with verifyTxParams', function () {
assert.strictEqual(verifyParams.txPrebuild.txHex, 'mock-tx-hex');
assert.deepStrictEqual(verifyParams.txParams, verifyTxParams.txParams);
});

it('should throw when verifyTxParams is provided without txHex or TSS txRequestId', async function () {
const signParams: WalletSignTransactionOptions = {
txPrebuild: {},
verifyTxParams: {
txParams: {
recipients: [{ address: 'test-address', amount: '1000' }],
},
},
};

await wallet.signTransaction(signParams).should.be.rejectedWith(InvalidTransactionError);
sinon.assert.notCalled(verifyTransactionStub);
});
});
6 changes: 4 additions & 2 deletions modules/sdk-core/src/bitgo/utils/tss/baseTSSUtils.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { EncryptionVersion, IRequestTracer } from '../../../api';
import * as openpgp from 'openpgp';
import { Key, readKey, SerializedKeyPair } from 'openpgp';
import { IBaseCoin, KeychainsTriplet } from '../../baseCoin';
import { IBaseCoin, KeychainsTriplet, TransactionParams } from '../../baseCoin';
import { BitGoBase } from '../../bitgoBase';
import { Keychain, KeyIndices, WebauthnKeyEncryptionInfo } from '../../keychain';
import { getTxRequest } from '../../tss';
Expand Down Expand Up @@ -269,7 +269,9 @@ export default class BaseTssUtils<KeyShare> extends MpcUtils implements ITssUtil
txRequest: string | TxRequest,
externalSignerCommitmentGenerator: CustomCommitmentGeneratingFunction,
externalSignerRShareGenerator: CustomRShareGeneratingFunction,
externalSignerGShareGenerator: CustomGShareGeneratingFunction
externalSignerGShareGenerator: CustomGShareGeneratingFunction,
_reqId?: IRequestTracer,
_txParams?: TransactionParams
): Promise<TxRequest> {
throw new Error('Method not implemented.');
}
Expand Down
4 changes: 3 additions & 1 deletion modules/sdk-core/src/bitgo/utils/tss/baseTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -915,7 +915,9 @@ export interface ITssUtils<KeyShare = EDDSA.KeyShare> {
txRequest: string | TxRequest,
externalSignerCommitmentGenerator: CustomCommitmentGeneratingFunction,
externalSignerRShareGenerator: CustomRShareGeneratingFunction,
externalSignerGShareGenerator: CustomGShareGeneratingFunction
externalSignerGShareGenerator: CustomGShareGeneratingFunction,
reqId?: IRequestTracer,
txParams?: TransactionParams
): Promise<TxRequest>;
signEcdsaTssUsingExternalSigner(
params: TSSParams | TSSParamsForMessage,
Expand Down
Loading
Loading