Skip to content

Commit d703b9a

Browse files
committed
fix: multiple issues with message signing
Ticket: BG-61620
1 parent 1031caf commit d703b9a

File tree

10 files changed

+157
-43
lines changed

10 files changed

+157
-43
lines changed

modules/bitgo/test/v2/unit/wallet.ts

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ import {
1717
TssUtils,
1818
TxRequest,
1919
Wallet,
20+
SignatureShareType,
21+
Ecdsa,
22+
Keychains,
2023
} from '@bitgo/sdk-core';
2124

2225
import { TestBitGo } from '@bitgo/sdk-test';
@@ -2034,6 +2037,7 @@ describe('V2 Wallet:', function () {
20342037
});
20352038

20362039
describe('Message Signing', function () {
2040+
const txHash = '0xrrrsss1b';
20372041
const txRequestForMessageSigning: TxRequest = {
20382042
txRequestId: 'id',
20392043
transactions: [],
@@ -2050,6 +2054,12 @@ describe('V2 Wallet:', function () {
20502054
walletId: 'walletId',
20512055
unsignedTxs: [],
20522056
unsignedMessages: [],
2057+
messages: [{
2058+
state: 'signed',
2059+
signatureShares: [{ from: SignatureShareType.USER, to: SignatureShareType.USER, share: '' }],
2060+
combineSigShare: '0:rrr:sss:3',
2061+
txHash,
2062+
}],
20532063
};
20542064
let signTxRequestForMessage;
20552065
const messageSigningCoins = ['teth', 'tpolygon'];
@@ -2058,10 +2068,13 @@ describe('V2 Wallet:', function () {
20582068
beforeEach(async function () {
20592069
signTxRequestForMessage = sandbox.stub(ECDSAUtils.EcdsaUtils.prototype, 'signTxRequestForMessage');
20602070
signTxRequestForMessage.resolves(txRequestForMessageSigning);
2071+
sandbox.stub(Keychains.prototype, 'getKeysForSigning').resolves([{ commonKeychain: 'test', id: '', pub: '' }]);
2072+
sinon.stub(Ecdsa.prototype, 'verify').resolves(true);
20612073
});
20622074

20632075
afterEach(async function () {
20642076
sinon.restore();
2077+
nock.cleanAll();
20652078
});
20662079

20672080
it('should throw error for unsupported coins', async function () {
@@ -2088,7 +2101,7 @@ describe('V2 Wallet:', function () {
20882101
message: { messageRaw, txRequestId },
20892102
prv: 'secretKey',
20902103
});
2091-
signMessage.should.deepEqual({ txRequestId } );
2104+
signMessage.should.deepEqual(txHash);
20922105
const actualArg = signMessageTssSpy.getCalls()[0].args[0];
20932106
actualArg.message.messageEncoded.should.equal(`\u0019Ethereum Signed Message:\n${messageRaw.length}${messageRaw}`);
20942107
});
@@ -2105,7 +2118,7 @@ describe('V2 Wallet:', function () {
21052118
message: { messageRaw },
21062119
prv: 'secretKey',
21072120
});
2108-
signMessage.should.deepEqual({ txRequestId } );
2121+
signMessage.should.deepEqual(txHash);
21092122
const actualArg = signMessageTssSpy.getCalls()[0].args[0];
21102123
actualArg.message.messageEncoded.should.equal(`\u0019Ethereum Signed Message:\n${messageRaw.length}${messageRaw}`);
21112124
});
@@ -2121,7 +2134,7 @@ describe('V2 Wallet:', function () {
21212134
message: { messageRaw },
21222135
prv: 'secretKey',
21232136
});
2124-
signMessage.should.deepEqual({ txRequestId } );
2137+
signMessage.should.deepEqual(txHash);
21252138
const actualArg = signMessageTssSpy.getCalls()[0].args[0];
21262139
actualArg.message.messageEncoded.should.equal(`\u0019Ethereum Signed Message:\n${messageRaw.length}${messageRaw}`);
21272140
});
@@ -2131,7 +2144,7 @@ describe('V2 Wallet:', function () {
21312144
reqId,
21322145
message: { messageRaw, txRequestId },
21332146
prv: '',
2134-
}).should.be.rejectedWith('prv required to sign message with TSS');
2147+
}).should.be.rejectedWith('keychain does not have property encryptedPrv');
21352148
});
21362149
});
21372150

modules/sdk-coin-eth/src/eth.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,14 @@ import {
4141
HalfSignedTransaction,
4242
FullySignedTransaction,
4343
MPCAlgorithm,
44+
ECDSAMethods,
4445
} from '@bitgo/sdk-core';
4546

4647
import { BaseCoin as StaticsBaseCoin, EthereumNetwork, ethGasConfigs } from '@bitgo/statics';
4748
import type * as EthTxLib from '@ethereumjs/tx';
4849
import type * as EthCommon from '@ethereumjs/common';
4950
import { calculateForwarderV1Address, getProxyInitcode } from './lib';
51+
import { intToHex, stripHexPrefix, addHexPrefix } from 'ethereumjs-util';
5052

5153
export { Recipient, HalfSignedTransaction, FullySignedTransaction };
5254

@@ -1752,6 +1754,25 @@ export class Eth extends BaseCoin {
17521754
return prefix.concat(message);
17531755
}
17541756

1757+
/**
1758+
* Create the final signed message hash from the combine share
1759+
* @param combineShare
1760+
*/
1761+
constructFinalSignedMessageHash(combineShare: string): string {
1762+
const combineSigArray = combineShare.split(ECDSAMethods.delimeter);
1763+
const signature = {
1764+
recid: parseInt(combineSigArray[0], 10),
1765+
r: combineSigArray[1],
1766+
s: combineSigArray[2],
1767+
y: combineSigArray[3],
1768+
};
1769+
const v = 27 + signature.recid;
1770+
const vHex = intToHex(v);
1771+
const vStr = stripHexPrefix(vHex);
1772+
const finalSig = addHexPrefix(signature.r.concat(signature.s, vStr));
1773+
return finalSig;
1774+
}
1775+
17551776
private isETHAddress(address: string): boolean {
17561777
return !!address.match(/0x[a-fA-F0-9]{40}/);
17571778
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export interface CoinThatConstructFinalSignedMessageHash {
2+
constructFinalSignedMessageHash(combineShare: string): string;
3+
}
4+
5+
export function isCoinThatConstructFinalSignedMessageHash(c: unknown): c is CoinThatConstructFinalSignedMessageHash {
6+
return typeof (c as any).constructFinalSignedMessageHash === 'function';
7+
}

modules/sdk-core/src/bitgo/tss/ecdsa/ecdsa.ts

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,13 @@ export async function sendShareToBitgo(
208208
const kShare = share as KShare;
209209
signatureShare = convertKShare(kShare);
210210
await sendSignatureShare(bitgo, walletId, txRequestId, signatureShare, requestType, signerShare, 'ecdsa');
211-
responseFromBitgo = await getBitgoToUserLatestShare(bitgo, walletId, txRequestId, ReceivedShareType.AShare);
211+
responseFromBitgo = await getBitgoToUserLatestShare(
212+
bitgo,
213+
walletId,
214+
txRequestId,
215+
ReceivedShareType.AShare,
216+
requestType
217+
);
212218
break;
213219
case SendShareType.MUShare:
214220
const shareToSend = share as MuDShare;
@@ -220,7 +226,13 @@ export async function sendShareToBitgo(
220226
share: `${muShareRecord.share}${secondaryDelimeter}${dShareRecord.share}`,
221227
};
222228
await sendSignatureShare(bitgo, walletId, txRequestId, signatureShare, requestType, signerShare, 'ecdsa');
223-
responseFromBitgo = await getBitgoToUserLatestShare(bitgo, walletId, txRequestId, ReceivedShareType.DShare);
229+
responseFromBitgo = await getBitgoToUserLatestShare(
230+
bitgo,
231+
walletId,
232+
txRequestId,
233+
ReceivedShareType.DShare,
234+
requestType
235+
);
224236
break;
225237
case SendShareType.SShare:
226238
const sShare = share as SShare;
@@ -247,11 +259,22 @@ export async function getBitgoToUserLatestShare(
247259
bitgo: BitGoBase,
248260
walletId: string,
249261
txRequestId: string,
250-
shareType: ReceivedShareType
262+
shareType: ReceivedShareType,
263+
requestType: RequestType
251264
): Promise<SendShareToBitgoRT> {
252265
let responseFromBitgo: SendShareToBitgoRT;
253266
const txRequest = await getTxRequest(bitgo, walletId, txRequestId);
254-
const userShares = txRequest.transactions[0].signatureShares;
267+
let userShares;
268+
switch (requestType) {
269+
case RequestType.tx:
270+
assert(txRequest.transactions, 'transactions required as part of txRequest');
271+
userShares = txRequest.transactions[0].signatureShares;
272+
break;
273+
case RequestType.message:
274+
assert(txRequest.messages, 'messages required as part of txRequest');
275+
userShares = txRequest.messages[0].signatureShares;
276+
break;
277+
}
255278
if (!userShares || !userShares.length) {
256279
throw new Error('user share is not present');
257280
}

modules/sdk-core/src/bitgo/tss/eddsa/eddsa.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { BaseTransaction } from './../../../account-lib/baseCoin/baseTransaction
2020
import { Ed25519BIP32 } from './../../../account-lib/mpc/hdTree';
2121
import _ = require('lodash');
2222
import { getTxRequest, sendSignatureShare } from '../common';
23+
import assert from 'assert';
2324

2425
export { getTxRequest, sendSignatureShare };
2526

@@ -228,8 +229,13 @@ export async function getBitgoToUserRShare(
228229
txRequestId: string
229230
): Promise<SignatureShareRecord> {
230231
const txRequest = await getTxRequest(bitgo, walletId, txRequestId);
231-
const signatureShares =
232-
txRequest.apiVersion === 'full' ? txRequest.transactions[0].signatureShares : txRequest.signatureShares;
232+
let signatureShares;
233+
if (txRequest.apiVersion === 'full') {
234+
assert(txRequest.transactions, 'transactions required as part of txRequest');
235+
signatureShares = txRequest.transactions[0].signatureShares;
236+
} else {
237+
signatureShares = txRequest.signatureShares;
238+
}
233239
if (_.isNil(signatureShares) || _.isEmpty(signatureShares)) {
234240
throw new Error(`No signatures shares found for id: ${txRequestId}`);
235241
}

modules/sdk-core/src/bitgo/utils/tss/baseTSSUtils.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@ import {
1818
TxRequest,
1919
TxRequestVersion,
2020
BackupKeyShare,
21-
IntentOptionsBase,
22-
PopulatedIntentBase,
21+
IntentOptionsForMessage,
22+
PopulatedIntentForMessageSigning,
2323
} from './baseTypes';
2424
import { SignShare, YShare, GShare } from '../../../account-lib/mpc/tss/eddsa/types';
2525

@@ -204,16 +204,18 @@ export default class BaseTssUtils<KeyShare> extends MpcUtils implements ITssUtil
204204
* @param preview
205205
*/
206206
async createTxRequestWithIntentForMessageSigning(
207-
params: IntentOptionsBase,
207+
params: IntentOptionsForMessage,
208208
apiVersion: TxRequestVersion = 'full',
209209
preview?: boolean
210210
): Promise<TxRequest> {
211-
const intentOptions: PopulatedIntentBase = {
211+
const intentOptions: PopulatedIntentForMessageSigning = {
212212
intentType: params.intentType,
213213
sequenceId: params.sequenceId,
214214
comment: params.comment,
215215
memo: params.memo?.value,
216216
isTss: params.isTss,
217+
messageRaw: params.messageRaw,
218+
messageEncoded: params.messageEncoded ?? '',
217219
};
218220
const whitelistedParams = {
219221
intent: {

modules/sdk-core/src/bitgo/utils/tss/baseTypes.ts

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ export interface TokenTransferRecipientParams {
5656
tokenId?: string;
5757
decimalPlaces?: number;
5858
}
59-
export interface IntentOptionsBase {
59+
interface IntentOptionsBase {
6060
reqId: IRequestTracer;
6161
intentType: string;
6262
sequenceId?: string;
@@ -67,6 +67,11 @@ export interface IntentOptionsBase {
6767
custodianMessageId?: string;
6868
}
6969

70+
export interface IntentOptionsForMessage extends IntentOptionsBase {
71+
messageRaw: string;
72+
messageEncoded?: string;
73+
}
74+
7075
export interface PrebuildTransactionWithIntentOptions extends IntentOptionsBase {
7176
recipients?: {
7277
address?: string;
@@ -95,14 +100,19 @@ export interface IntentRecipient {
95100
data?: string;
96101
tokenData?: TokenTransferRecipientParams;
97102
}
98-
export interface PopulatedIntentBase {
103+
interface PopulatedIntentBase {
99104
intentType: string;
100105
sequenceId?: string;
101106
comment?: string;
102107
memo?: string;
103108
isTss?: boolean;
104109
}
105110

111+
export interface PopulatedIntentForMessageSigning extends PopulatedIntentBase {
112+
messageRaw: string;
113+
messageEncoded: string;
114+
}
115+
106116
export interface PopulatedIntent extends PopulatedIntentBase {
107117
recipients?: IntentRecipient[];
108118
nonce?: string;
@@ -185,11 +195,17 @@ export type TxRequest = {
185195
// Only available in 'lite' version
186196
unsignedTxs: UnsignedTransactionTss[]; // Should override with blockchain / sig scheme specific unsigned tx
187197
// Only available in 'full' version
188-
transactions: {
198+
transactions?: {
189199
state: TransactionState;
190200
unsignedTx: UnsignedTransactionTss; // Should override with blockchain / sig specific unsigned tx
191201
signatureShares: SignatureShareRecord[];
192202
}[];
203+
messages?: {
204+
state: TransactionState;
205+
signatureShares: SignatureShareRecord[];
206+
combineSigShare?: string;
207+
txHash?: string;
208+
}[];
193209
apiVersion?: TxRequestVersion;
194210
latest: boolean;
195211
};
@@ -213,7 +229,8 @@ export type TSSParams = {
213229
};
214230

215231
export type TSSParamsForMessage = TSSParams & {
216-
finalMessage: string;
232+
messageRaw: string;
233+
messageEncoded?: string;
217234
};
218235

219236
export interface BitgoHeldBackupKeyShare {

modules/sdk-core/src/bitgo/utils/tss/ecdsa/ecdsa.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -587,13 +587,13 @@ export class EcdsaUtils extends baseTSSUtils<KeyShare> {
587587
let signablePayload;
588588

589589
if (requestType === RequestType.tx) {
590+
assert(txRequestResolved.transactions, 'Unable to find transactions in txRequest');
590591
signablePayload = Buffer.from(txRequestResolved.transactions[0].unsignedTx.signableHex, 'hex');
591592
} else if (requestType === RequestType.message) {
592-
const finalMessage = (params as TSSParamsForMessage).finalMessage;
593-
assert(finalMessage);
594-
signablePayload = Buffer.from(finalMessage, 'hex');
593+
const finalMessage = (params as TSSParamsForMessage).messageEncoded;
594+
assert(finalMessage, 'finalMessage is required');
595+
signablePayload = Buffer.from(finalMessage);
595596
}
596-
597597
const userSShare = await ECDSAMethods.createUserSignatureShare(
598598
userOmicronAndDeltaShare.oShare,
599599
bitgoToUserDShare,
@@ -630,8 +630,8 @@ export class EcdsaUtils extends baseTSSUtils<KeyShare> {
630630
* @returns {Promise<TxRequest>} fully signed TxRequest object
631631
*/
632632
async signTxRequestForMessage(params: TSSParamsForMessage): Promise<TxRequest> {
633-
if (!params.finalMessage) {
634-
throw new Error('finalMessage required to sign message');
633+
if (!params.messageRaw) {
634+
throw new Error('Raw message required to sign message');
635635
}
636636
return this.signRequestBase(params, RequestType.message);
637637
}

modules/sdk-core/src/bitgo/utils/tss/eddsa/eddsa.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import {
2626
import { KeyShare, YShare } from './types';
2727
import baseTSSUtils from '../baseTSSUtils';
2828
import { KeychainsTriplet } from '../../../baseCoin';
29+
import assert from 'assert';
2930

3031
/**
3132
* Utility functions for TSS work flows.
@@ -284,6 +285,7 @@ export class EddsaUtils extends baseTSSUtils<KeyShare> {
284285
throw new Error('Invalid user key - missing backupYShare');
285286
}
286287

288+
assert(txRequestResolved.transactions, 'Unable to find transactions in txRequest');
287289
const unsignedTx =
288290
txRequestResolved.apiVersion === 'full'
289291
? txRequestResolved.transactions[0].unsignedTx
@@ -323,6 +325,7 @@ export class EddsaUtils extends baseTSSUtils<KeyShare> {
323325
throw new Error('Invalid user key - missing backupYShare');
324326
}
325327

328+
assert(txRequestResolved.transactions, 'Unable to find transactions in txRequest');
326329
const unsignedTx =
327330
txRequestResolved.apiVersion === 'full'
328331
? txRequestResolved.transactions[0].unsignedTx
@@ -403,6 +406,7 @@ export class EddsaUtils extends baseTSSUtils<KeyShare> {
403406
throw new Error('Invalid user key - missing backupYShare');
404407
}
405408

409+
assert(txRequestResolved.transactions, 'Unable to find transactions in txRequest');
406410
const unsignedTx =
407411
txRequestResolved.apiVersion === 'full'
408412
? txRequestResolved.transactions[0].unsignedTx

0 commit comments

Comments
 (0)