Skip to content

Commit dc5bee0

Browse files
committed
test: add ecdsaMPCv2 offline signing tests
TICKET: HSM-1513
1 parent e3bc07c commit dc5bee0

2 files changed

Lines changed: 428 additions & 0 deletions

File tree

Lines changed: 394 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,394 @@
1+
import * as assert from 'assert';
2+
import * as sinon from 'sinon';
3+
import { Hash, randomBytes } from 'crypto';
4+
import createKeccakHash from 'keccak';
5+
import {
6+
MPCv2PartyFromStringOrNumber,
7+
MPCv2SignatureShareRound1Input,
8+
MPCv2SignatureShareRound1Output,
9+
MPCv2SignatureShareRound2Input,
10+
MPCv2SignatureShareRound2Output,
11+
MPCv2SignatureShareRound3Input,
12+
} from '@bitgo/public-types';
13+
import { DklsComms, DklsDsg, DklsTypes, DklsUtils } from '@bitgo/sdk-lib-mpc';
14+
import * as sjcl from '@bitgo/sjcl';
15+
import {
16+
BitGoBase,
17+
EcdsaMPCv2Utils,
18+
IBaseCoin,
19+
SignatureShareRecord,
20+
SignatureShareType,
21+
TxRequest,
22+
} from '../../../../../../src';
23+
import { bitgoGpgKey } from './gpgKeys';
24+
25+
describe('ECDSA MPC v2', () => {
26+
it('should sign a message hash using ECDSA MPC v2 offline rounds', async () => {
27+
const [userShare, backupShare, bitgoShare] = await DklsUtils.generateDKGKeyShares();
28+
29+
assert.ok(userShare);
30+
assert.ok(backupShare);
31+
assert.ok(bitgoShare);
32+
33+
const mockBg = {} as BitGoBase;
34+
mockBg.getEnv = sinon.stub().returns('test');
35+
mockBg.encrypt = sinon.stub().callsFake((params) => {
36+
const salt = randomBytes(8);
37+
const iv = randomBytes(16);
38+
return sjcl.encrypt(params.password, params.input, {
39+
salt: [bytesToWord(salt.subarray(0, 4)), bytesToWord(salt.subarray(4))],
40+
iv: [
41+
bytesToWord(iv.subarray(0, 4)),
42+
bytesToWord(iv.subarray(4, 8)),
43+
bytesToWord(iv.subarray(8, 12)),
44+
bytesToWord(iv.subarray(12, 16)),
45+
],
46+
adata: params.adata,
47+
});
48+
});
49+
mockBg.decrypt = sinon.stub().callsFake((params) => {
50+
return sjcl.decrypt(params.password, params.input);
51+
});
52+
53+
const mockCoin = {} as IBaseCoin;
54+
mockCoin.getHashFunction = sinon.stub().callsFake(() => createKeccakHash('keccak256') as Hash);
55+
56+
const ecdsaMPCv2Utils = new EcdsaMPCv2Utils(mockBg, mockCoin);
57+
58+
const walletID = '62fe536a6b4cf70007acb48c0e7bb0b0';
59+
const tMessage = 'testMessage';
60+
const derivationPath = 'm/0';
61+
const walletPassphrase = 'testPass';
62+
63+
// round 1
64+
const reqMPCv2SigningRound1 = {
65+
txRequest: {
66+
txRequestId: '123456',
67+
apiVersion: 'full',
68+
walletId: walletID,
69+
transactions: [
70+
{
71+
unsignedTx: {
72+
derivationPath,
73+
signableHex: tMessage,
74+
},
75+
signatureShares: [],
76+
},
77+
],
78+
},
79+
prv: userShare.getKeyShare().toString('base64'),
80+
walletPassphrase,
81+
};
82+
83+
const resMPCv2SigningRound1 = await ecdsaMPCv2Utils.createOfflineRound1Share(reqMPCv2SigningRound1 as any);
84+
resMPCv2SigningRound1.should.have.property('signatureShareRound1');
85+
resMPCv2SigningRound1.should.have.property('userGpgPubKey');
86+
resMPCv2SigningRound1.should.have.property('encryptedRound1Session');
87+
resMPCv2SigningRound1.should.have.property('encryptedUserGpgPrvKey');
88+
89+
const encryptedRound1Session = resMPCv2SigningRound1.encryptedRound1Session;
90+
const encryptedUserGpgPrvKey = resMPCv2SigningRound1.encryptedUserGpgPrvKey;
91+
92+
const hashBuffer = mockCoin.getHashFunction().update(Buffer.from(tMessage, 'hex')).digest();
93+
const bitgoSession = new DklsDsg.Dsg(bitgoShare.getKeyShare(), 2, derivationPath, hashBuffer);
94+
95+
const txRequestRound1 = await signBitgoMPCv2Round1(
96+
bitgoSession,
97+
reqMPCv2SigningRound1.txRequest as any,
98+
resMPCv2SigningRound1.signatureShareRound1,
99+
resMPCv2SigningRound1.userGpgPubKey
100+
);
101+
assert.ok(
102+
txRequestRound1.transactions &&
103+
txRequestRound1.transactions.length === 1 &&
104+
txRequestRound1.transactions[0].signatureShares.length === 2,
105+
'txRequestRound2.transactions is not an array of length 1 with 2 signatureShares'
106+
);
107+
108+
// round 2
109+
const reqMPCv2SigningRound2 = {
110+
txRequest: txRequestRound1,
111+
encryptedRound1Session,
112+
encryptedUserGpgPrvKey,
113+
bitgoPublicGpgKey: bitgoGpgKey.public,
114+
prv: userShare.getKeyShare().toString('base64'),
115+
walletPassphrase,
116+
};
117+
118+
const resMPCv2SigningRound2 = await ecdsaMPCv2Utils.createOfflineRound2Share(reqMPCv2SigningRound2 as any);
119+
resMPCv2SigningRound2.should.have.property('signatureShareRound2');
120+
resMPCv2SigningRound2.should.have.property('encryptedRound2Session');
121+
122+
const encryptedRound2Session = resMPCv2SigningRound2.encryptedRound2Session;
123+
124+
const { txRequest: txRequestRound2, bitgoMsg4 } = await signBitgoMPCv2Round2(
125+
bitgoSession,
126+
reqMPCv2SigningRound2.txRequest,
127+
resMPCv2SigningRound2.signatureShareRound2,
128+
resMPCv2SigningRound1.userGpgPubKey
129+
);
130+
assert.ok(
131+
txRequestRound2.transactions &&
132+
txRequestRound2.transactions.length === 1 &&
133+
txRequestRound2.transactions[0].signatureShares.length === 4,
134+
'txRequestRound2.transactions is not an array of length 1 with 4 signatureShares'
135+
);
136+
bitgoMsg4.should.have.property('signatureR');
137+
138+
// round 3
139+
const reqMPCv2SigningRound3 = {
140+
txRequest: txRequestRound2,
141+
encryptedRound2Session,
142+
encryptedUserGpgPrvKey,
143+
bitgoPublicGpgKey: bitgoGpgKey.public,
144+
prv: userShare.getKeyShare().toString('base64'),
145+
walletPassphrase,
146+
};
147+
148+
const resMPCv2SigningRound3 = await ecdsaMPCv2Utils.createOfflineRound3Share(reqMPCv2SigningRound3 as any);
149+
resMPCv2SigningRound3.should.have.property('signatureShareRound3');
150+
151+
const { userMsg4 } = await signBitgoMPCv2Round3(
152+
bitgoSession,
153+
resMPCv2SigningRound3.signatureShareRound3,
154+
resMPCv2SigningRound1.userGpgPubKey
155+
);
156+
157+
// signature generation and validation
158+
assert.ok(userMsg4.data.msg4.signatureR === bitgoMsg4.signatureR, 'User and BitGo signaturesR do not match');
159+
160+
const deserializedBitgoMsg4 = DklsTypes.deserializeMessages({
161+
p2pMessages: [],
162+
broadcastMessages: [bitgoMsg4],
163+
});
164+
165+
const deserializedUserMsg4 = DklsTypes.deserializeMessages({
166+
p2pMessages: [],
167+
broadcastMessages: [
168+
{
169+
from: userMsg4.data.msg4.from,
170+
payload: userMsg4.data.msg4.message,
171+
},
172+
],
173+
});
174+
175+
const combinedSigUsingUtil = DklsUtils.combinePartialSignatures(
176+
[deserializedUserMsg4.broadcastMessages[0].payload, deserializedBitgoMsg4.broadcastMessages[0].payload],
177+
Buffer.from(userMsg4.data.msg4.signatureR, 'base64').toString('hex')
178+
);
179+
180+
const convertedSignature = DklsUtils.verifyAndConvertDklsSignature(
181+
Buffer.from(tMessage, 'hex'),
182+
combinedSigUsingUtil,
183+
DklsTypes.getCommonKeychain(userShare.getKeyShare()),
184+
derivationPath,
185+
createKeccakHash('keccak256') as Hash
186+
);
187+
assert.ok(convertedSignature, 'Signature is not valid');
188+
assert.ok(convertedSignature.split(':').length === 4, 'Signature is not valid');
189+
});
190+
});
191+
192+
function bytesToWord(bytes?: Uint8Array | number[]): number {
193+
if (!(bytes instanceof Uint8Array) || bytes.length !== 4) {
194+
throw new Error('bytes must be a Uint8Array with length 4');
195+
}
196+
197+
return bytes.reduce((num, byte) => num * 0x100 + byte, 0);
198+
}
199+
200+
function getUserPartyGpgKeyPublic(userPubKey: string): DklsTypes.PartyGpgKey {
201+
return {
202+
partyId: 0,
203+
gpgKey: userPubKey,
204+
};
205+
}
206+
207+
function getBitGoPartyGpgKeyPrv(bitgoPrvKey: string): DklsTypes.PartyGpgKey {
208+
return {
209+
partyId: 2,
210+
gpgKey: bitgoPrvKey,
211+
};
212+
}
213+
214+
async function signBitgoMPCv2Round1(
215+
bitgoSession: DklsDsg.Dsg,
216+
txRequest: TxRequest,
217+
userShare: SignatureShareRecord,
218+
userGPGPubKey: string
219+
): Promise<TxRequest> {
220+
assert.ok(
221+
txRequest.transactions && txRequest.transactions.length === 1,
222+
'txRequest.transactions is not an array of length 1'
223+
);
224+
txRequest.transactions[0].signatureShares.push(userShare);
225+
// Do the actual signing on BitGo's side based on User's messages
226+
const signatureShare = JSON.parse(userShare.share) as MPCv2SignatureShareRound1Input;
227+
const deserializedMessages = DklsTypes.deserializeMessages({
228+
p2pMessages: [],
229+
broadcastMessages: [
230+
{
231+
from: signatureShare.data.msg1.from,
232+
payload: signatureShare.data.msg1.message,
233+
},
234+
],
235+
});
236+
const bitgoToUserRound1BroadcastMsg = await bitgoSession.init();
237+
const bitgoToUserRound2Msg = bitgoSession.handleIncomingMessages({
238+
p2pMessages: [],
239+
broadcastMessages: deserializedMessages.broadcastMessages,
240+
});
241+
const serializedBitGoToUserRound1And2Msgs = DklsTypes.serializeMessages({
242+
p2pMessages: bitgoToUserRound2Msg.p2pMessages,
243+
broadcastMessages: [bitgoToUserRound1BroadcastMsg],
244+
});
245+
246+
const authEncMessages = await DklsComms.encryptAndAuthOutgoingMessages(
247+
serializedBitGoToUserRound1And2Msgs,
248+
[getUserPartyGpgKeyPublic(userGPGPubKey)],
249+
[getBitGoPartyGpgKeyPrv(bitgoGpgKey.private)]
250+
);
251+
252+
const bitgoToUserSignatureShare: MPCv2SignatureShareRound1Output = {
253+
type: 'round1Output',
254+
data: {
255+
msg1: {
256+
from: authEncMessages.broadcastMessages[0].from as MPCv2PartyFromStringOrNumber,
257+
signature: authEncMessages.broadcastMessages[0].payload.signature,
258+
message: authEncMessages.broadcastMessages[0].payload.message,
259+
},
260+
msg2: {
261+
from: authEncMessages.p2pMessages[0].from as MPCv2PartyFromStringOrNumber,
262+
to: authEncMessages.p2pMessages[0].to as MPCv2PartyFromStringOrNumber,
263+
encryptedMessage: authEncMessages.p2pMessages[0].payload.encryptedMessage,
264+
signature: authEncMessages.p2pMessages[0].payload.signature,
265+
},
266+
},
267+
};
268+
txRequest.transactions[0].signatureShares.push({
269+
from: SignatureShareType.BITGO,
270+
to: SignatureShareType.USER,
271+
share: JSON.stringify(bitgoToUserSignatureShare),
272+
});
273+
return txRequest;
274+
}
275+
276+
async function signBitgoMPCv2Round2(
277+
bitgoSession: DklsDsg.Dsg,
278+
txRequest: TxRequest,
279+
userShare: SignatureShareRecord,
280+
userGPGPubKey: string
281+
): Promise<{ txRequest: TxRequest; bitgoMsg4: DklsTypes.SerializedBroadcastMessage }> {
282+
assert.ok(
283+
txRequest.transactions && txRequest.transactions.length === 1,
284+
'txRequest.transactions is not an array of length 1'
285+
);
286+
txRequest.transactions[0].signatureShares.push(userShare);
287+
288+
// Do the actual signing on BitGo's side based on User's messages
289+
const parsedSignatureShare = JSON.parse(userShare.share) as MPCv2SignatureShareRound2Input;
290+
const serializedMessages = await DklsComms.decryptAndVerifyIncomingMessages(
291+
{
292+
p2pMessages: [
293+
{
294+
from: parsedSignatureShare.data.msg2.from,
295+
to: parsedSignatureShare.data.msg2.to,
296+
payload: {
297+
encryptedMessage: parsedSignatureShare.data.msg2.encryptedMessage,
298+
signature: parsedSignatureShare.data.msg2.signature,
299+
},
300+
},
301+
{
302+
from: parsedSignatureShare.data.msg3.from,
303+
to: parsedSignatureShare.data.msg3.to,
304+
payload: {
305+
encryptedMessage: parsedSignatureShare.data.msg3.encryptedMessage,
306+
signature: parsedSignatureShare.data.msg3.signature,
307+
},
308+
},
309+
],
310+
broadcastMessages: [],
311+
},
312+
[getUserPartyGpgKeyPublic(userGPGPubKey)],
313+
[getBitGoPartyGpgKeyPrv(bitgoGpgKey.private)]
314+
);
315+
const deserializedMessages2 = DklsTypes.deserializeMessages({
316+
p2pMessages: [serializedMessages.p2pMessages[0]],
317+
broadcastMessages: [],
318+
});
319+
320+
const bitgoToUserRound3Msg = bitgoSession.handleIncomingMessages(deserializedMessages2);
321+
const serializedBitGoToUserRound3Msgs = DklsTypes.serializeMessages(bitgoToUserRound3Msg);
322+
323+
const authEncMessages = await DklsComms.encryptAndAuthOutgoingMessages(
324+
serializedBitGoToUserRound3Msgs,
325+
[getUserPartyGpgKeyPublic(userGPGPubKey)],
326+
[getBitGoPartyGpgKeyPrv(bitgoGpgKey.private)]
327+
);
328+
329+
const bitgoToUserSignatureShare: MPCv2SignatureShareRound2Output = {
330+
type: 'round2Output',
331+
data: {
332+
msg3: {
333+
from: authEncMessages.p2pMessages[0].from as MPCv2PartyFromStringOrNumber,
334+
to: authEncMessages.p2pMessages[0].to as MPCv2PartyFromStringOrNumber,
335+
encryptedMessage: authEncMessages.p2pMessages[0].payload.encryptedMessage,
336+
signature: authEncMessages.p2pMessages[0].payload.signature,
337+
},
338+
},
339+
};
340+
341+
// handling user msg3 but not returning bitgo msg4 since its stored on bitgo side only
342+
const deserializedMessages3 = DklsTypes.deserializeMessages({
343+
p2pMessages: [serializedMessages.p2pMessages[1]],
344+
broadcastMessages: [],
345+
});
346+
const deserializedBitgoMsg4 = bitgoSession.handleIncomingMessages(deserializedMessages3);
347+
const serializedBitGoToUserRound4Msgs = DklsTypes.serializeMessages(deserializedBitgoMsg4);
348+
349+
txRequest.transactions[0].signatureShares.push({
350+
from: SignatureShareType.BITGO,
351+
to: SignatureShareType.USER,
352+
share: JSON.stringify(bitgoToUserSignatureShare),
353+
});
354+
return { txRequest, bitgoMsg4: serializedBitGoToUserRound4Msgs.broadcastMessages[0] };
355+
}
356+
357+
async function signBitgoMPCv2Round3(
358+
bitgoSession: DklsDsg.Dsg,
359+
userShare: SignatureShareRecord,
360+
userGPGPubKey: string
361+
): Promise<{ userMsg4: MPCv2SignatureShareRound3Input }> {
362+
const parsedSignatureShare = JSON.parse(userShare.share) as MPCv2SignatureShareRound3Input;
363+
const msg4 = parsedSignatureShare.data.msg4;
364+
const signatureRAuthMessage =
365+
msg4.signatureR && msg4.signatureRSignature
366+
? { message: msg4.signatureR, signature: msg4.signatureRSignature }
367+
: undefined;
368+
const serializedMessages = await DklsComms.decryptAndVerifyIncomingMessages(
369+
{
370+
p2pMessages: [],
371+
broadcastMessages: [
372+
{
373+
from: msg4.from,
374+
payload: {
375+
message: msg4.message,
376+
signature: msg4.signature,
377+
},
378+
signatureR: signatureRAuthMessage,
379+
},
380+
],
381+
},
382+
[getUserPartyGpgKeyPublic(userGPGPubKey)],
383+
[getBitGoPartyGpgKeyPrv(bitgoGpgKey.private)]
384+
);
385+
const deserializedMessages = DklsTypes.deserializeMessages({
386+
p2pMessages: [],
387+
broadcastMessages: [serializedMessages.broadcastMessages[0]],
388+
});
389+
bitgoSession.handleIncomingMessages(deserializedMessages);
390+
391+
return {
392+
userMsg4: parsedSignatureShare,
393+
};
394+
}

0 commit comments

Comments
 (0)