@bitpay-labs/bitcore-wallet-service 11.10.14 → 11.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/package.json +5 -5
  2. package/src/lib/common/constants.ts +1 -1
  3. package/src/lib/errors/errordefinitions.ts +8 -0
  4. package/src/lib/model/tsskeygen.ts +12 -4
  5. package/src/lib/routes/middleware/authRequest.ts +6 -16
  6. package/src/lib/routes/middleware/authTssRequest.ts +130 -26
  7. package/src/lib/routes/tss.ts +21 -8
  8. package/src/lib/routes/wallets.ts +8 -9
  9. package/src/lib/server.ts +9 -17
  10. package/src/lib/storage.ts +55 -22
  11. package/src/lib/tss.ts +77 -1
  12. package/test/integration/tss.test.ts +706 -371
  13. package/test/storage.test.ts +6 -7
  14. package/ts_build/package.json +4 -4
  15. package/ts_build/src/lib/common/constants.d.ts +1 -1
  16. package/ts_build/src/lib/common/constants.js +1 -1
  17. package/ts_build/src/lib/common/index.d.ts +1 -1
  18. package/ts_build/src/lib/errors/errordefinitions.d.ts +4 -0
  19. package/ts_build/src/lib/errors/errordefinitions.d.ts.map +1 -1
  20. package/ts_build/src/lib/errors/errordefinitions.js +4 -0
  21. package/ts_build/src/lib/errors/errordefinitions.js.map +1 -1
  22. package/ts_build/src/lib/model/tsskeygen.d.ts +2 -0
  23. package/ts_build/src/lib/model/tsskeygen.d.ts.map +1 -1
  24. package/ts_build/src/lib/model/tsskeygen.js +6 -2
  25. package/ts_build/src/lib/model/tsskeygen.js.map +1 -1
  26. package/ts_build/src/lib/routes/middleware/authRequest.d.ts.map +1 -1
  27. package/ts_build/src/lib/routes/middleware/authRequest.js +6 -16
  28. package/ts_build/src/lib/routes/middleware/authRequest.js.map +1 -1
  29. package/ts_build/src/lib/routes/middleware/authTssRequest.d.ts +2 -1
  30. package/ts_build/src/lib/routes/middleware/authTssRequest.d.ts.map +1 -1
  31. package/ts_build/src/lib/routes/middleware/authTssRequest.js +104 -24
  32. package/ts_build/src/lib/routes/middleware/authTssRequest.js.map +1 -1
  33. package/ts_build/src/lib/routes/tss.d.ts.map +1 -1
  34. package/ts_build/src/lib/routes/tss.js +19 -7
  35. package/ts_build/src/lib/routes/tss.js.map +1 -1
  36. package/ts_build/src/lib/routes/wallets.d.ts.map +1 -1
  37. package/ts_build/src/lib/routes/wallets.js +8 -9
  38. package/ts_build/src/lib/routes/wallets.js.map +1 -1
  39. package/ts_build/src/lib/server.d.ts.map +1 -1
  40. package/ts_build/src/lib/server.js +8 -18
  41. package/ts_build/src/lib/server.js.map +1 -1
  42. package/ts_build/src/lib/storage.d.ts +9 -2
  43. package/ts_build/src/lib/storage.d.ts.map +1 -1
  44. package/ts_build/src/lib/storage.js +51 -20
  45. package/ts_build/src/lib/storage.js.map +1 -1
  46. package/ts_build/src/lib/tss.d.ts +8 -0
  47. package/ts_build/src/lib/tss.d.ts.map +1 -1
  48. package/ts_build/src/lib/tss.js +59 -1
  49. package/ts_build/src/lib/tss.js.map +1 -1
  50. package/ts_build/test/integration/tss.test.d.ts.map +1 -1
  51. package/ts_build/test/integration/tss.test.js +599 -357
  52. package/ts_build/test/integration/tss.test.js.map +1 -1
  53. package/ts_build/test/storage.test.js +6 -7
  54. package/ts_build/test/storage.test.js.map +1 -1
@@ -37,7 +37,6 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
37
37
  };
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
39
  const sinon_1 = __importDefault(require("sinon"));
40
- const chai = __importStar(require("chai"));
41
40
  require("chai/register-should");
42
41
  const crypto_wallet_core_1 = require("@bitpay-labs/crypto-wallet-core");
43
42
  const supertest_1 = __importDefault(require("supertest"));
@@ -48,8 +47,16 @@ const tss_1 = require("../../src/lib/tss");
48
47
  const tsssign_1 = require("../../src/lib/model/tsssign");
49
48
  const tsskeygen_1 = require("../../src/lib/model/tsskeygen");
50
49
  const index_2 = require("../../src/lib/common/index");
51
- const should = chai.should();
52
50
  const { Utils } = index_2.Common;
51
+ function xPubToCopayerId(chain, xpub) {
52
+ chain = chain.toLowerCase();
53
+ const str = chain == 'btc' ? xpub : (chain + xpub);
54
+ const hash = crypto_wallet_core_1.BitcoreLib.crypto.Hash.sha256(Buffer.from(str));
55
+ return hash.toString('hex');
56
+ }
57
+ function getXPubKey(seed, network = 'testnet') {
58
+ return crypto_wallet_core_1.BitcoreLib.HDPrivateKey.fromSeed(seed, network).hdPublicKey.toString();
59
+ }
53
60
  describe('TSS', function () {
54
61
  this.timeout(Math.max(this['_timeout'], 2000));
55
62
  const vector = {
@@ -77,6 +84,8 @@ describe('TSS', function () {
77
84
  messages: {
78
85
  round0: {
79
86
  party0: {
87
+ round: 0,
88
+ partyId: 0,
80
89
  p2pMessages: [],
81
90
  broadcastMessages: [{
82
91
  from: 0,
@@ -88,6 +97,8 @@ describe('TSS', function () {
88
97
  }]
89
98
  },
90
99
  party1: {
100
+ round: 0,
101
+ partyId: 1,
91
102
  p2pMessages: [],
92
103
  broadcastMessages: [{
93
104
  from: 1,
@@ -99,6 +110,8 @@ describe('TSS', function () {
99
110
  }]
100
111
  },
101
112
  party2: {
113
+ round: 0,
114
+ partyId: 2,
102
115
  p2pMessages: [],
103
116
  broadcastMessages: [{
104
117
  from: 2,
@@ -112,6 +125,8 @@ describe('TSS', function () {
112
125
  },
113
126
  round1: {
114
127
  party0: {
128
+ round: 1,
129
+ partyId: 0,
115
130
  p2pMessages: [{
116
131
  from: 0,
117
132
  to: 1,
@@ -126,6 +141,8 @@ describe('TSS', function () {
126
141
  broadcastMessages: []
127
142
  },
128
143
  party1: {
144
+ round: 1,
145
+ partyId: 1,
129
146
  p2pMessages: [{
130
147
  from: 1,
131
148
  to: 0,
@@ -140,6 +157,8 @@ describe('TSS', function () {
140
157
  broadcastMessages: []
141
158
  },
142
159
  party2: {
160
+ round: 1,
161
+ partyId: 2,
143
162
  p2pMessages: [{
144
163
  from: 2,
145
164
  to: 0,
@@ -156,6 +175,8 @@ describe('TSS', function () {
156
175
  },
157
176
  round2: {
158
177
  party0: {
178
+ round: 2,
179
+ partyId: 0,
159
180
  p2pMessages: [{
160
181
  from: 0,
161
182
  to: 1,
@@ -170,6 +191,8 @@ describe('TSS', function () {
170
191
  broadcastMessages: []
171
192
  },
172
193
  party1: {
194
+ round: 2,
195
+ partyId: 1,
173
196
  p2pMessages: [{
174
197
  from: 1,
175
198
  to: 0,
@@ -184,6 +207,8 @@ describe('TSS', function () {
184
207
  broadcastMessages: []
185
208
  },
186
209
  party2: {
210
+ round: 2,
211
+ partyId: 2,
187
212
  p2pMessages: [{
188
213
  from: 2,
189
214
  to: 0,
@@ -200,6 +225,8 @@ describe('TSS', function () {
200
225
  },
201
226
  round3: {
202
227
  party0: {
228
+ round: 3,
229
+ partyId: 0,
203
230
  p2pMessages: [],
204
231
  broadcastMessages: [
205
232
  {
@@ -213,6 +240,8 @@ describe('TSS', function () {
213
240
  ],
214
241
  },
215
242
  party1: {
243
+ round: 3,
244
+ partyId: 1,
216
245
  p2pMessages: [],
217
246
  broadcastMessages: [
218
247
  {
@@ -226,6 +255,8 @@ describe('TSS', function () {
226
255
  ],
227
256
  },
228
257
  party2: {
258
+ round: 3,
259
+ partyId: 2,
229
260
  p2pMessages: [],
230
261
  broadcastMessages: [
231
262
  {
@@ -249,11 +280,75 @@ describe('TSS', function () {
249
280
  messageHash: '516ab037171ee1a7787cbe07c28948dd0252ea60b7f40ee6ffbb7e1271f691ea'
250
281
  }]
251
282
  };
252
- const n = 2;
253
- const m = 3;
254
283
  let app;
284
+ let storage;
255
285
  const urlPrefix = '/bws/api';
256
286
  const sandbox = sinon_1.default.createSandbox();
287
+ const SESSION_ID = 'test';
288
+ const copayer1 = xPubToCopayerId('btc', getXPubKey(vector.party0.seed));
289
+ const copayer2 = xPubToCopayerId('btc', getXPubKey(vector.party1.seed));
290
+ const copayer3 = xPubToCopayerId('btc', getXPubKey(vector.party2.seed));
291
+ const parties = [vector.party0, vector.party1, vector.party2];
292
+ const copayerIds = [copayer1, copayer2, copayer3];
293
+ const { round0, round1 } = vector.keygen.messages;
294
+ const round0Messages = [round0.party0, round0.party1, round0.party2];
295
+ const round1Messages = [round1.party0, round1.party1, round1.party2];
296
+ const authPubKey = (partyId) => parties[partyId].authKey.publicKey.toString();
297
+ const partyAuth = (partyId) => ({ copayerId: copayerIds[partyId], privKey: parties[partyId].authKey });
298
+ const randomKey = () => new crypto_wallet_core_1.BitcoreLib.PrivateKey();
299
+ const keyGenMessage = (messages, partyId) => ({
300
+ ...JSON.parse(JSON.stringify(messages[partyId])),
301
+ publicKey: authPubKey(partyId)
302
+ });
303
+ const round0Message = (partyId) => keyGenMessage(round0Messages, partyId);
304
+ const round1Message = (partyId) => keyGenMessage(round1Messages, partyId);
305
+ const signRequest = (method, path, body, privKey) => {
306
+ const hash = Utils.hashMessage(`${method}|${path}|${JSON.stringify(body)}`, false);
307
+ return crypto_wallet_core_1.BitcoreLib.crypto.ECDSA.sign(hash, privKey, { endian: 'little' }).toString();
308
+ };
309
+ const post = (path, body, auth) => app.post(urlPrefix + path)
310
+ .set('x-identity', auth.copayerId)
311
+ .set('x-signature', signRequest('post', path, body, auth.privKey))
312
+ .send(body);
313
+ const get = (path, auth) => app.get(urlPrefix + path)
314
+ .set('x-identity', auth.copayerId)
315
+ .set('x-signature', signRequest('get', path, {}, auth.privKey));
316
+ const expectError = (res, status, code, message) => {
317
+ res.status.should.equal(status);
318
+ res.body.code.should.equal(code);
319
+ if (message) {
320
+ res.body.message.should.equal(message);
321
+ }
322
+ };
323
+ const expireSession = () => {
324
+ const now = Date.now() + 2 * 60 * 1000;
325
+ sandbox.stub(Date, 'now').returns(now);
326
+ };
327
+ const createKeyGenSession = async (opts = {}) => {
328
+ const { version = 1.2, registered = [], joined = [], timeLimit } = opts;
329
+ const doc = tsskeygen_1.TssKeyGenModel.create({
330
+ id: SESSION_ID,
331
+ message: round0Message(0),
332
+ n: vector.n,
333
+ copayerId: copayerIds[0],
334
+ version,
335
+ timeLimit
336
+ });
337
+ if (version <= 1.1) {
338
+ delete doc.authPublicKeys;
339
+ }
340
+ for (const partyId of registered) {
341
+ doc.authPublicKeys[partyId] = authPubKey(partyId);
342
+ }
343
+ for (const partyId of joined) {
344
+ if (version > 1.1) {
345
+ doc.authPublicKeys[partyId] = authPubKey(partyId);
346
+ }
347
+ doc.participants[partyId] = copayerIds[partyId];
348
+ doc.rounds[0].push({ fromPartyId: partyId, messages: round0Message(partyId) });
349
+ }
350
+ await storage.storeTssKeyGenSession({ doc });
351
+ };
257
352
  before(async function () {
258
353
  await helpers_1.default.before();
259
354
  });
@@ -261,7 +356,7 @@ describe('TSS', function () {
261
356
  await helpers_1.default.after();
262
357
  });
263
358
  beforeEach(function (done) {
264
- const storage = helpers_1.default.getStorage();
359
+ storage = helpers_1.default.getStorage();
265
360
  const blockchainExplorerMock = helpers_1.default.getBlockchainExplorer();
266
361
  const expressApp = new index_1.default.ExpressApp();
267
362
  expressApp.start({
@@ -278,384 +373,531 @@ describe('TSS', function () {
278
373
  afterEach(function () {
279
374
  sandbox.restore();
280
375
  });
281
- describe('POST /v1/tss/keygen/:id', function () {
282
- const url = id => `${urlPrefix}/v1/tss/keygen/${id}`;
376
+ describe('Key generation', function () {
283
377
  beforeEach(async function () {
284
- const storage = helpers_1.default.getStorage();
285
378
  await storage.db.collection(index_1.default.Storage.collections.TSS_KEYGEN).deleteMany({});
286
379
  });
287
- describe('middleware', function () {
288
- beforeEach(function () {
289
- sandbox.stub(tss_1.TssKeyGen, 'processMessage').resolves();
290
- });
291
- it('should verify tss broadcast message', function (done) {
292
- const message = JSON.parse(JSON.stringify(vector.keygen.messages.round0.party0));
293
- message.publicKey = vector.party0.authKey.publicKey.toString();
294
- app.post(url('test'))
295
- .send({ message })
296
- .expect(200)
297
- .end((err, res) => {
298
- should.not.exist(err);
299
- done();
300
- });
301
- });
302
- it('should verify tss p2p message', function (done) {
303
- const message = JSON.parse(JSON.stringify(vector.keygen.messages.round1.party0));
304
- message.publicKey = vector.party0.authKey.publicKey.toString();
305
- app.post(url('test'))
306
- .send({ message })
307
- .expect(200)
308
- .end((err, res) => {
309
- should.not.exist(err);
310
- done();
311
- });
312
- });
313
- it('should error if no publicKey', function (done) {
314
- app.post(url('test'))
315
- .send({})
316
- .expect(200)
317
- .end((err, res) => {
318
- should.exist(err);
319
- res.status.should.equal(400);
320
- res.body.code.should.equal('TSS_PUBKEY_MISSING');
321
- done();
322
- });
323
- });
324
- it('should error if publicKey is bogus', function (done) {
325
- const message = JSON.parse(JSON.stringify(vector.keygen.messages.round0.party0));
326
- message.publicKey = 'invalid';
327
- app.post(url('test'))
328
- .send({ message })
329
- .expect(200)
330
- .end((err, res) => {
331
- should.exist(err);
332
- res.status.should.equal(400);
333
- res.body.code.should.equal('TSS_INVALID_MESSAGE');
334
- done();
335
- });
336
- });
337
- it('should error if publicKey cannot verify broadcast message signature', function (done) {
338
- const message = JSON.parse(JSON.stringify(vector.keygen.messages.round0.party0));
339
- message.publicKey = new crypto_wallet_core_1.BitcoreLib.PrivateKey().publicKey.toString();
340
- app.post(url('test'))
341
- .send({ message })
342
- .expect(200)
343
- .end((err, res) => {
344
- should.exist(err);
345
- res.status.should.equal(400);
346
- res.body.code.should.equal('TSS_INVALID_MESSAGE_SIG');
347
- done();
348
- });
349
- });
350
- it('should error if messages are empty arrays', function (done) {
351
- const message = {
352
- broadcastMessages: [],
353
- p2pMessages: [],
354
- publicKey: new crypto_wallet_core_1.BitcoreLib.PrivateKey().publicKey.toString()
380
+ describe('POST /v1/tss/keygen/:id', function () {
381
+ const path = `/v1/tss/keygen/${SESSION_ID}`;
382
+ describe('initiating a session', function () {
383
+ it('should create a new session from party 0\'s round 0 message', async function () {
384
+ const res = await post(path, { message: round0Message(0), n: vector.n, version: 1.2 }, partyAuth(0));
385
+ res.status.should.equal(200);
386
+ const session = await storage.fetchTssKeyGenSession({ id: SESSION_ID });
387
+ session.schemeVersion.should.equal(1.2);
388
+ session.participants.should.deep.equal([copayerIds[0], null, null]);
389
+ session.authPublicKeys.should.deep.equal([authPubKey(0), null, null]);
390
+ });
391
+ it('should error if x-identity is missing', async function () {
392
+ const res = await app.post(urlPrefix + path).send({ message: round0Message(0), n: vector.n, version: 1.2 });
393
+ expectError(res, 401, 'NOT_AUTHORIZED');
394
+ });
395
+ it('should error if a party other than 0 tries to start the session', async function () {
396
+ const res = await post(path, { message: round0Message(1), n: vector.n, version: 1.2 }, partyAuth(1));
397
+ expectError(res, 401, 'NOT_AUTHORIZED', 'Session not found');
398
+ });
399
+ it('should error if the request is not signed by the message\'s publicKey', async function () {
400
+ const res = await post(path, { message: round0Message(0), n: vector.n, version: 1.2 }, { copayerId: copayerIds[0], privKey: randomKey() });
401
+ expectError(res, 401, 'NOT_AUTHORIZED', 'Invalid signature');
402
+ });
403
+ it('should error if the message has no publicKey', async function () {
404
+ const message = round0Message(0);
405
+ delete message.publicKey;
406
+ const res = await post(path, { message, n: vector.n, version: 1.2 }, partyAuth(0));
407
+ expectError(res, 401, 'NOT_AUTHORIZED');
408
+ });
409
+ it('should error if party 0 tries to start the session with a non-round 0 message', async function () {
410
+ const res = await post(path, { message: round1Message(0), n: vector.n, version: 1.2 }, partyAuth(0));
411
+ expectError(res, 401, 'NOT_AUTHORIZED', 'Session not found');
412
+ });
413
+ });
414
+ describe('message verification', function () {
415
+ beforeEach(function () {
416
+ sandbox.stub(tss_1.TssKeyGen, 'processMessage').resolves();
417
+ });
418
+ it('should verify tss broadcast message', async function () {
419
+ const res = await post(path, { message: round0Message(0) }, partyAuth(0));
420
+ res.status.should.equal(200);
421
+ });
422
+ it('should verify tss p2p message', async function () {
423
+ await createKeyGenSession({ joined: [1, 2] });
424
+ const res = await post(path, { message: round1Message(0) }, partyAuth(0));
425
+ res.status.should.equal(200);
426
+ });
427
+ it('should error if no publicKey', async function () {
428
+ await createKeyGenSession();
429
+ const message = round0Message(0);
430
+ delete message.publicKey;
431
+ const res = await post(path, { message }, partyAuth(0));
432
+ expectError(res, 400, 'TSS_PUBKEY_MISSING');
433
+ });
434
+ it('should error if publicKey is bogus', async function () {
435
+ await createKeyGenSession();
436
+ const message = { ...round0Message(0), publicKey: 'invalid' };
437
+ const res = await post(path, { message }, partyAuth(0));
438
+ expectError(res, 400, 'TSS_INVALID_MESSAGE');
439
+ });
440
+ it('should error if publicKey cannot verify broadcast message signature', async function () {
441
+ await createKeyGenSession();
442
+ const message = { ...round0Message(0), publicKey: randomKey().publicKey.toString() };
443
+ const res = await post(path, { message }, partyAuth(0));
444
+ expectError(res, 400, 'TSS_INVALID_MESSAGE_SIG');
445
+ });
446
+ it('should error if messages are empty arrays', async function () {
447
+ const message = {
448
+ round: 0,
449
+ partyId: 0,
450
+ broadcastMessages: [],
451
+ p2pMessages: [],
452
+ publicKey: authPubKey(0)
453
+ };
454
+ const res = await post(path, { message }, partyAuth(0));
455
+ expectError(res, 400, 'TSS_INVALID_MESSAGE');
456
+ });
457
+ });
458
+ describe('joining a v1.2 session', function () {
459
+ it('should let a registered party join', async function () {
460
+ await createKeyGenSession({ registered: [1] });
461
+ const res = await post(path, { message: round0Message(1), version: 1.2 }, partyAuth(1));
462
+ res.status.should.equal(200);
463
+ const session = await storage.fetchTssKeyGenSession({ id: SESSION_ID });
464
+ session.participants[1].should.equal(copayerIds[1]);
465
+ session.rounds[0].map(r => r.fromPartyId).should.deep.equal([0, 1]);
466
+ });
467
+ it('should let registered parties join out of partyId order', async function () {
468
+ await createKeyGenSession({ registered: [1, 2] });
469
+ for (const partyId of [2, 1]) {
470
+ const res = await post(path, { message: round0Message(partyId), version: 1.2 }, partyAuth(partyId));
471
+ res.status.should.equal(200);
472
+ }
473
+ const session = await storage.fetchTssKeyGenSession({ id: SESSION_ID });
474
+ session.participants.should.deep.equal(copayerIds);
475
+ });
476
+ it('should error if the party was not registered by the initiator', async function () {
477
+ await createKeyGenSession({ registered: [1] });
478
+ const res = await post(path, { message: round0Message(2), version: 1.2 }, partyAuth(2));
479
+ expectError(res, 401, 'NOT_AUTHORIZED', 'Not authorized');
480
+ });
481
+ it('should error if the message has no publicKey', async function () {
482
+ await createKeyGenSession({ registered: [1] });
483
+ const message = round0Message(1);
484
+ delete message.publicKey;
485
+ const res = await post(path, { message, version: 1.2 }, partyAuth(1));
486
+ expectError(res, 401, 'NOT_AUTHORIZED', 'Not authorized');
487
+ });
488
+ it('should error if the request is not signed by the registered key', async function () {
489
+ await createKeyGenSession({ registered: [1] });
490
+ const res = await post(path, { message: round0Message(1), version: 1.2 }, { copayerId: copayerIds[1], privKey: randomKey() });
491
+ expectError(res, 401, 'NOT_AUTHORIZED', 'Invalid signature');
492
+ });
493
+ it('should error if the party joins at a partyId other than the one it was registered at', async function () {
494
+ await createKeyGenSession({ registered: [1] });
495
+ const message = { ...round0Message(1), partyId: 2 };
496
+ const res = await post(path, { message, version: 1.2 }, partyAuth(1));
497
+ expectError(res, 401, 'NOT_AUTHORIZED', 'Copayer not found in session');
498
+ });
499
+ it('should error if the registered party slot has already been taken', async function () {
500
+ await createKeyGenSession({ joined: [1] });
501
+ const res = await post(path, { message: round0Message(1), version: 1.2 }, { copayerId: copayerIds[2], privKey: parties[1].authKey });
502
+ expectError(res, 401, 'NOT_AUTHORIZED', 'Not authorized');
503
+ });
504
+ it('should error if a registered party that has not joined sends a non-round 0 message', async function () {
505
+ await createKeyGenSession({ registered: [1] });
506
+ const res = await post(path, { message: round1Message(1), version: 1.2 }, partyAuth(1));
507
+ expectError(res, 401, 'NOT_AUTHORIZED', 'Copayer not found in session');
508
+ });
509
+ it('should error if session is expired', async function () {
510
+ await createKeyGenSession({ registered: [1], timeLimit: 1 });
511
+ expireSession();
512
+ const res = await post(path, { message: round0Message(1), version: 1.2 }, partyAuth(1));
513
+ expectError(res, 400, 'TSS_SESSION_EXPIRED');
514
+ });
515
+ });
516
+ describe('joining a legacy (v1.1) session', function () {
517
+ it('should let a party join without being registered', async function () {
518
+ await createKeyGenSession({ version: 1.1 });
519
+ const res = await post(path, { message: round0Message(1), version: 1.1 }, partyAuth(1));
520
+ res.status.should.equal(200);
521
+ const session = await storage.fetchTssKeyGenSession({ id: SESSION_ID });
522
+ session.participants[1].should.equal(copayerIds[1]);
523
+ });
524
+ it('should authenticate a joined party with the publicKey from its round 0 message', async function () {
525
+ sandbox.stub(tss_1.TssKeyGen, 'processMessage').resolves();
526
+ await createKeyGenSession({ version: 1.1, joined: [1, 2] });
527
+ const res = await post(path, { message: round1Message(1), version: 1.1 }, partyAuth(1));
528
+ res.status.should.equal(200);
529
+ });
530
+ it('should error if a joined party signs with a key other than its round 0 publicKey', async function () {
531
+ await createKeyGenSession({ version: 1.1, joined: [1, 2] });
532
+ const res = await post(path, { message: round1Message(1), version: 1.1 }, { copayerId: copayerIds[1], privKey: randomKey() });
533
+ expectError(res, 401, 'NOT_AUTHORIZED', 'Invalid signature');
534
+ });
535
+ });
536
+ describe('concurrent joins', function () {
537
+ const stubJoinRaceCondition = (concurrentPartyId, concurrentCopayerId) => {
538
+ const stub = sandbox.stub(index_1.default.Storage.prototype, 'storeTssKeyGenParticipant');
539
+ stub.callThrough();
540
+ stub.onFirstCall().callsFake(async function (params) {
541
+ const current = await this.fetchTssKeyGenSession({ id: params.id });
542
+ await stub.wrappedMethod.call(this, { id: params.id, partyId: concurrentPartyId, copayerId: concurrentCopayerId, __v: current.__v });
543
+ return stub.wrappedMethod.call(this, params);
544
+ });
545
+ return stub;
355
546
  };
356
- app.post(url('test'))
357
- .send({ message })
358
- .expect(200)
359
- .end((err, res) => {
360
- should.exist(err);
361
- res.status.should.equal(400);
362
- res.body.code.should.equal('TSS_INVALID_MESSAGE');
363
- done();
547
+ it('should retry and join if another party joined concurrently', async function () {
548
+ await createKeyGenSession({ registered: [1, 2] });
549
+ const stub = stubJoinRaceCondition(2, copayerIds[2]);
550
+ const res = await post(path, { message: round0Message(1), version: 1.2 }, partyAuth(1));
551
+ res.status.should.equal(200);
552
+ stub.callCount.should.equal(2);
553
+ const session = await storage.fetchTssKeyGenSession({ id: SESSION_ID });
554
+ session.participants.should.deep.equal(copayerIds);
555
+ session.rounds[0].map(r => r.fromPartyId).should.deep.equal([0, 1]);
556
+ });
557
+ it('should not join if another copayer took the same partyId concurrently', async function () {
558
+ await createKeyGenSession({ version: 1.1 });
559
+ stubJoinRaceCondition(1, copayerIds[2]);
560
+ const res = await post(path, { message: round0Message(1), version: 1.1 }, partyAuth(1));
561
+ res.status.should.not.equal(200);
562
+ const session = await storage.fetchTssKeyGenSession({ id: SESSION_ID });
563
+ session.participants[1].should.equal(copayerIds[2]);
564
+ session.rounds[0].map(r => r.fromPartyId).should.deep.equal([0]);
565
+ });
566
+ it('should not retry on other storage errors', async function () {
567
+ await createKeyGenSession({ registered: [1] });
568
+ const stub = sandbox.stub(index_1.default.Storage.prototype, 'storeTssKeyGenParticipant').rejects(new Error('boom'));
569
+ const res = await post(path, { message: round0Message(1), version: 1.2 }, partyAuth(1));
570
+ res.status.should.equal(500);
571
+ stub.callCount.should.equal(1);
572
+ });
573
+ });
574
+ describe('existing participants', function () {
575
+ it('should error if the request signature is invalid', async function () {
576
+ await createKeyGenSession({ joined: [1, 2] });
577
+ const res = await post(path, { message: round1Message(0), version: 1.2 }, { copayerId: copayerIds[0], privKey: randomKey() });
578
+ expectError(res, 401, 'NOT_AUTHORIZED', 'Invalid signature');
579
+ });
580
+ it('should error if session is expired', async function () {
581
+ await createKeyGenSession({ joined: [1, 2], timeLimit: 1 });
582
+ expireSession();
583
+ const res = await post(path, { message: round1Message(0), version: 1.2 }, partyAuth(0));
584
+ expectError(res, 400, 'TSS_SESSION_EXPIRED');
364
585
  });
365
586
  });
366
587
  });
367
- it('should error if session is expired', function (done) {
368
- const storage = helpers_1.default.getStorage();
369
- const publicKey = vector.party0.authKey.publicKey.toString();
370
- const keyGen = tsskeygen_1.TssKeyGenModel.create({
371
- id: 'test',
372
- message: { partyId: 0, publicKey },
373
- n: 2,
374
- copayerId: publicKey,
375
- version: 1.1,
376
- timeLimit: 1,
377
- });
378
- storage.storeTssKeyGenSession({ doc: keyGen }).then(() => {
379
- const now = Date.now() + 2 * 60 * 1000;
380
- sandbox.stub(Date, 'now').returns(now);
381
- const message = JSON.parse(JSON.stringify(vector.keygen.messages.round1.party0));
382
- message.publicKey = publicKey;
383
- app.post(url('test'))
384
- .set('x-identity', publicKey)
385
- .send({ message })
386
- .expect((res) => {
387
- res.status.should.equal(400);
388
- res.body.code.should.equal('TSS_SESSION_EXPIRED');
389
- })
390
- .end(done);
391
- }).catch(done);
392
- });
393
- });
394
- describe('GET /v1/tss/keygen/:id/:round', function () {
395
- const roundUrl = '/v1/tss/keygen/test/0?maxWaitTime=0';
396
- const parties = [vector.party0, vector.party1, vector.party2];
397
- const round0Messages = [vector.keygen.messages.round0.party0, vector.keygen.messages.round0.party1, vector.keygen.messages.round0.party2];
398
- const joinRound0 = async (partyId) => {
399
- const publicKey = parties[partyId].authKey.publicKey.toString();
400
- const message = { ...round0Messages[partyId], partyId, round: 0, publicKey };
401
- await app.post(`${urlPrefix}/v1/tss/keygen/test`)
402
- .set('x-identity', publicKey)
403
- .send({ message, n: 3, version: 1.1 })
404
- .expect(200);
405
- };
406
- const getRound0 = (partyId) => {
407
- const { authKey } = parties[partyId];
408
- const hash = Utils.hashMessage(`get|${roundUrl}|{}`, false);
409
- const xSignature = crypto_wallet_core_1.BitcoreLib.crypto.ECDSA.sign(hash, authKey, { endian: 'little' }).toString();
410
- return app.get(urlPrefix + roundUrl)
411
- .set('x-identity', authKey.publicKey.toString())
412
- .set('x-signature', xSignature);
413
- };
414
- beforeEach(async function () {
415
- const storage = helpers_1.default.getStorage();
416
- await storage.db.collection(index_1.default.Storage.collections.TSS_KEYGEN).deleteMany({});
417
- });
418
- it('should authenticate every party when round 0 messages arrive in partyId order', async function () {
419
- await joinRound0(0);
420
- await joinRound0(1);
421
- await joinRound0(2);
422
- for (const partyId of [0, 1, 2]) {
423
- const res = await getRound0(partyId);
588
+ describe('POST /v1/tss/keygen/:id/register', function () {
589
+ const path = `/v1/tss/keygen/${SESSION_ID}/register`;
590
+ const register = (partyId, partyPubKey, auth = partyAuth(0)) => post(path, { partyId, partyPubKey }, auth);
591
+ it('should let the initiator register a party', async function () {
592
+ await createKeyGenSession();
593
+ const res = await register(1, authPubKey(1));
424
594
  res.status.should.equal(200);
425
- JSON.parse(res.text).messages.length.should.equal(2);
595
+ const session = await storage.fetchTssKeyGenSession({ id: SESSION_ID });
596
+ session.authPublicKeys.should.deep.equal([authPubKey(0), authPubKey(1), null]);
597
+ });
598
+ it('should be idempotent when re-registering the same key at the same partyId', async function () {
599
+ await createKeyGenSession();
600
+ (await register(1, authPubKey(1))).status.should.equal(200);
601
+ (await register(1, authPubKey(1))).status.should.equal(200);
602
+ const session = await storage.fetchTssKeyGenSession({ id: SESSION_ID });
603
+ session.authPublicKeys[1].should.equal(authPubKey(1));
604
+ session.__v.should.equal(1);
605
+ });
606
+ it('should error if the key is already registered at a different partyId', async function () {
607
+ await createKeyGenSession();
608
+ (await register(1, authPubKey(1))).status.should.equal(200);
609
+ const res = await register(2, authPubKey(1));
610
+ expectError(res, 400, 'TSS_PARTY_ALREADY_REGISTERED');
611
+ });
612
+ it('should error if registering the initiator\'s own key', async function () {
613
+ await createKeyGenSession();
614
+ const res = await register(1, authPubKey(0));
615
+ expectError(res, 400, 'TSS_PARTY_ALREADY_REGISTERED');
616
+ });
617
+ for (const [description, partyId, getPubKey] of [
618
+ ['partyId is 0', 0, () => authPubKey(1)],
619
+ ['partyId is negative', -1, () => authPubKey(1)],
620
+ ['partyId is not a number', 'abc', () => authPubKey(1)],
621
+ ['partyId is >= n', vector.n, () => authPubKey(1)],
622
+ ['partyPubKey is missing', 1, () => undefined],
623
+ ['partyPubKey is blank', 1, () => ' '],
624
+ ['partyPubKey is the initiator\'s copayerId', 1, () => copayerIds[0]],
625
+ ]) {
626
+ it(`should error if ${description}`, async function () {
627
+ await createKeyGenSession();
628
+ const res = await register(partyId, getPubKey());
629
+ expectError(res, 400, 'TSS_INVALID_PARTY_DATA');
630
+ });
426
631
  }
427
- });
428
- it('should authenticate every party when round 0 messages arrive out of partyId order', async function () {
429
- await joinRound0(0);
430
- await joinRound0(2);
431
- await joinRound0(1);
432
- for (const partyId of [0, 1, 2]) {
433
- const res = await getRound0(partyId);
632
+ it('should accept a numeric string partyId', async function () {
633
+ await createKeyGenSession();
634
+ const res = await register('1', authPubKey(1));
434
635
  res.status.should.equal(200);
435
- JSON.parse(res.text).messages.length.should.equal(2);
636
+ const session = await storage.fetchTssKeyGenSession({ id: SESSION_ID });
637
+ session.authPublicKeys[1].should.equal(authPubKey(1));
638
+ });
639
+ for (const [description, partyId, expected] of [
640
+ ['partyId is missing', undefined, 'undefined'],
641
+ ['partyId is null', null, 'null'],
642
+ ['partyId has a numeric prefix', '1abc', '1abc'],
643
+ ]) {
644
+ it(`should error with an invalid partyId message if ${description}`, async function () {
645
+ await createKeyGenSession();
646
+ const res = await register(partyId, authPubKey(1));
647
+ expectError(res, 400, 'TSS_INVALID_PARTY_DATA', `Invalid partyId: ${expected}`);
648
+ const session = await storage.fetchTssKeyGenSession({ id: SESSION_ID });
649
+ session.authPublicKeys.should.deep.equal([authPubKey(0), null, null]);
650
+ });
436
651
  }
652
+ it('should error if the party has already submitted its round 0 message', async function () {
653
+ await createKeyGenSession({ joined: [1] });
654
+ const res = await register(1, randomKey().publicKey.toString());
655
+ expectError(res, 400, 'TSS_ROUNDS_ALREADY_STARTED');
656
+ });
657
+ it('should error if a joined participant other than the initiator tries to register a party', async function () {
658
+ await createKeyGenSession({ joined: [1] });
659
+ const res = await register(2, authPubKey(2), partyAuth(1));
660
+ expectError(res, 400, 'TSS_NON_PARTICIPANT', 'Only the first participant can register a party');
661
+ });
662
+ it('should error if the requester is not in the session', async function () {
663
+ await createKeyGenSession();
664
+ const res = await register(1, authPubKey(1), partyAuth(1));
665
+ expectError(res, 401, 'NOT_AUTHORIZED', 'Not authorized');
666
+ });
667
+ it('should error if the request signature is invalid', async function () {
668
+ await createKeyGenSession();
669
+ const res = await register(1, authPubKey(1), { copayerId: copayerIds[0], privKey: randomKey() });
670
+ expectError(res, 401, 'NOT_AUTHORIZED', 'Invalid signature');
671
+ });
672
+ it('should error if the session does not exist', async function () {
673
+ const res = await register(1, authPubKey(1));
674
+ expectError(res, 401, 'NOT_AUTHORIZED', 'Session not found');
675
+ });
437
676
  });
438
- it('should reject another party identity when round 0 messages arrive out of partyId order', async function () {
439
- await joinRound0(0);
440
- await joinRound0(2);
441
- await joinRound0(1);
442
- for (const [partyId, otherPartyId] of [[1, 2], [2, 1]]) {
443
- const res = await getRound0(partyId)
444
- .set('x-identity', parties[otherPartyId].authKey.publicKey.toString());
445
- res.status.should.equal(401);
446
- res.body.code.should.equal('NOT_AUTHORIZED');
447
- res.body.message.should.equal('Invalid signature');
448
- }
677
+ describe('GET /v1/tss/keygen/:id/:round', function () {
678
+ const path = `/v1/tss/keygen/${SESSION_ID}/0?maxWaitTime=0`;
679
+ const expectRound0Messages = async (auth) => {
680
+ const res = await get(path, auth);
681
+ res.status.should.equal(200);
682
+ JSON.parse(res.text).messages.length.should.equal(vector.n - 1);
683
+ };
684
+ const runRound0 = async (joinOrder) => {
685
+ const keygenPath = `/v1/tss/keygen/${SESSION_ID}`;
686
+ (await post(keygenPath, { message: round0Message(0), n: vector.n, version: 1.2 }, partyAuth(0))).status.should.equal(200);
687
+ for (const partyId of [1, 2]) {
688
+ (await post(`${keygenPath}/register`, { partyId, partyPubKey: authPubKey(partyId) }, partyAuth(0))).status.should.equal(200);
689
+ }
690
+ for (const partyId of joinOrder) {
691
+ (await post(keygenPath, { message: round0Message(partyId), version: 1.2 }, partyAuth(partyId))).status.should.equal(200);
692
+ }
693
+ };
694
+ it('should authenticate every party when round 0 messages arrive in partyId order', async function () {
695
+ await runRound0([1, 2]);
696
+ for (const partyId of [0, 1, 2]) {
697
+ await expectRound0Messages(partyAuth(partyId));
698
+ }
699
+ });
700
+ it('should authenticate every party when round 0 messages arrive out of partyId order', async function () {
701
+ await runRound0([2, 1]);
702
+ for (const partyId of [0, 1, 2]) {
703
+ await expectRound0Messages(partyAuth(partyId));
704
+ }
705
+ });
706
+ it('should reject another party identity', async function () {
707
+ await createKeyGenSession({ joined: [1, 2] });
708
+ for (const [partyId, otherPartyId] of [[1, 2], [2, 1]]) {
709
+ const res = await get(path, { copayerId: copayerIds[otherPartyId], privKey: parties[partyId].authKey });
710
+ expectError(res, 401, 'NOT_AUTHORIZED', 'Invalid signature');
711
+ }
712
+ });
713
+ it('should error if a registered party has not joined yet', async function () {
714
+ await createKeyGenSession({ registered: [2], joined: [1] });
715
+ const res = await get(path, partyAuth(2));
716
+ expectError(res, 401, 'NOT_AUTHORIZED', 'Not authorized');
717
+ });
718
+ it('should error if the copayer is not in the session', async function () {
719
+ await createKeyGenSession({ joined: [1, 2] });
720
+ const res = await get(path, { copayerId: 'notInSession', privKey: randomKey() });
721
+ expectError(res, 401, 'NOT_AUTHORIZED', 'Not authorized');
722
+ });
723
+ it('should error if the session does not exist', async function () {
724
+ const res = await get(path, partyAuth(0));
725
+ expectError(res, 401, 'NOT_AUTHORIZED', 'Session not found');
726
+ });
727
+ it('should error if session is expired', async function () {
728
+ await createKeyGenSession({ joined: [1, 2], timeLimit: 1 });
729
+ expireSession();
730
+ const res = await get(path, partyAuth(0));
731
+ expectError(res, 400, 'TSS_SESSION_EXPIRED');
732
+ });
733
+ describe('legacy (v1.1) session', function () {
734
+ it('should authenticate every party with the publicKey from its round 0 message', async function () {
735
+ await createKeyGenSession({ version: 1.1, joined: [1, 2] });
736
+ for (const partyId of [0, 1, 2]) {
737
+ await expectRound0Messages(partyAuth(partyId));
738
+ }
739
+ });
740
+ it('should reject another party identity', async function () {
741
+ await createKeyGenSession({ version: 1.1, joined: [1, 2] });
742
+ for (const [partyId, otherPartyId] of [[1, 2], [2, 1]]) {
743
+ const res = await get(path, { copayerId: copayerIds[otherPartyId], privKey: parties[partyId].authKey });
744
+ expectError(res, 401, 'NOT_AUTHORIZED', 'Invalid signature');
745
+ }
746
+ });
747
+ it('should error if the copayer is not in the session', async function () {
748
+ await createKeyGenSession({ version: 1.1, joined: [1, 2] });
749
+ const res = await get(path, { copayerId: 'notInSession', privKey: randomKey() });
750
+ expectError(res, 401, 'NOT_AUTHORIZED', 'Copayer not found in session');
751
+ });
752
+ });
449
753
  });
450
754
  });
451
- describe('POST /v1/tss/sign/:id', function () {
452
- const url = id => `${urlPrefix}/v1/tss/sign/${id}`;
453
- const signRequest = body => {
454
- const message = `post|${url('test').replace(urlPrefix, '')}|${JSON.stringify(body)}`;
455
- const hash = Utils.hashMessage(message, false);
456
- const xSignature = crypto_wallet_core_1.BitcoreLib.crypto.ECDSA.sign(hash, privateKey, { endian: 'little' }).toString();
457
- return xSignature;
458
- };
459
- const privateKey = new crypto_wallet_core_1.BitcoreLib.PrivateKey('cd643c4f2e68b669d97c51877905e8d77efc98cabb5a36a8eda66bc143055d37');
460
- const publicKey = privateKey.publicKey.toString();
461
- beforeEach(async function () {
462
- const storage = helpers_1.default.getStorage();
463
- await storage.db.collection(index_1.default.Storage.collections.TSS_SIGN).deleteMany({});
464
- await storage.db.collection(index_1.default.Storage.collections.COPAYERS_LOOKUP).deleteMany({});
465
- await storage.db.collection(index_1.default.Storage.collections.COPAYERS_LOOKUP).insertOne({
466
- copayerId: publicKey,
467
- walletId: 'test',
468
- requestPubKey: publicKey,
469
- });
755
+ describe('Signing', function () {
756
+ const sigMessage = (partyId) => ({
757
+ round: 0,
758
+ partyId,
759
+ publicKey: authPubKey(partyId),
760
+ broadcastMessages: [],
761
+ p2pMessages: []
470
762
  });
471
- it('should error if session is expired', function (done) {
472
- const storage = helpers_1.default.getStorage();
473
- const sigGen = tsssign_1.TssSigGenModel.create({
474
- id: 'test',
475
- message: { partyId: 0, publicKey },
763
+ const createSigSession = async (opts = {}) => {
764
+ const doc = tsssign_1.TssSigGenModel.create({
765
+ id: SESSION_ID,
766
+ message: sigMessage(0),
476
767
  m: 2,
477
- copayerId: publicKey,
768
+ copayerId: copayerIds[0],
478
769
  version: 1.1,
479
- timeLimit: 1,
480
- });
481
- storage.storeTssSigSession({ doc: sigGen }).then(() => {
482
- const now = Date.now() + 2 * 60 * 1000;
483
- sandbox.stub(Date, 'now').returns(now);
484
- const message = { publicKey, p2pMessages: [{ payload: { encryptedMessage: 'encryptie', signature: 'siggie' } }], broadcastMessages: [] };
485
- const xSignature = signRequest({ message });
486
- app.post(url('test'))
487
- .set('x-identity', publicKey)
488
- .set('x-signature', xSignature)
489
- .send({ message })
490
- .expect((res) => {
491
- res.status.should.equal(400);
492
- res.body.code.should.equal('TSS_SESSION_EXPIRED');
493
- })
494
- .end(done);
495
- }).catch(done);
496
- });
497
- });
498
- describe('POST /v1/tss/sign/:id/store', function () {
499
- const url = id => `${urlPrefix}/v1/tss/sign/${id}/store`;
500
- const signRequest = body => {
501
- const message = `post|${url('test').replace(urlPrefix, '')}|${JSON.stringify(body)}`;
502
- const hash = Utils.hashMessage(message, false);
503
- const xSignature = crypto_wallet_core_1.BitcoreLib.crypto.ECDSA.sign(hash, privateKey, { endian: 'little' }).toString();
504
- return xSignature;
505
- };
506
- const privateKey = new crypto_wallet_core_1.BitcoreLib.PrivateKey('cd643c4f2e68b669d97c51877905e8d77efc98cabb5a36a8eda66bc143055d37');
507
- const publicKey = privateKey.publicKey.toString();
508
- const signature = {
509
- r: '0x71a8fb6b23203f7ae7f5df7dac410204d8b8d8b74e31c3919e0c764a00925f74',
510
- s: '0x18048c9ce4a860f6354b4bb795178ff3cde858ed3afef6c2e2c51c8fac8b9257',
511
- v: 1,
512
- pubKey: '0267c2c572fcc1a5026d0e5d8e60a04ff6adeaf1a3a996c9c69f3b1714cff1db85',
770
+ timeLimit: opts.timeLimit
771
+ });
772
+ doc.participants.push({ partyId: 1, copayerId: copayerIds[1] });
773
+ doc.rounds[0].push({ fromPartyId: 1, messages: sigMessage(1) });
774
+ await storage.storeTssSigSession({ doc });
513
775
  };
514
776
  beforeEach(async function () {
515
- const storage = helpers_1.default.getStorage();
516
777
  await storage.db.collection(index_1.default.Storage.collections.TSS_SIGN).deleteMany({});
517
- const sigGen = tsssign_1.TssSigGenModel.create({
518
- id: 'test',
519
- message: { partyId: 0, publicKey },
520
- m: 2,
521
- copayerId: publicKey,
522
- version: 1.1,
523
- });
524
- await storage.storeTssSigSession({ doc: sigGen });
525
- await storage.storeTssSignature({ id: 'test', signature });
778
+ await storage.db.collection(index_1.default.Storage.collections.COPAYERS_LOOKUP).deleteMany({});
779
+ await storage.db.collection(index_1.default.Storage.collections.COPAYERS_LOOKUP).insertMany([0, 1, 2].map(partyId => ({
780
+ copayerId: copayerIds[partyId],
781
+ walletId: 'test',
782
+ requestPubKey: authPubKey(partyId),
783
+ })));
526
784
  });
527
- it('should throw a mismatch error for a mismatched signature: r', function (done) {
528
- const modifiedSignature = {
529
- ...signature,
530
- r: '0x71a8fb6b23203f7ae7f5df7dac410204d8b8d8b74e31c3919e0c764a00925f75',
531
- };
532
- const xSignature = signRequest({ signature: modifiedSignature });
533
- app.post(url('test'))
534
- .set('x-identity', publicKey)
535
- .set('x-signature', xSignature)
536
- .send({
537
- signature: modifiedSignature
538
- })
539
- .expect(res => {
540
- res.status.should.equal(400);
541
- res.body.should.have.property('code');
542
- res.body.code.should.equal('TSS_FINAL_SIGNATURE_MISMATCH');
543
- res.body.message.should.equal('Final signature does not match the one generated by the session');
544
- })
545
- .end(done);
785
+ describe('POST /v1/tss/sign/:id', function () {
786
+ const path = `/v1/tss/sign/${SESSION_ID}`;
787
+ it('should error if session is expired', async function () {
788
+ await createSigSession({ timeLimit: 1 });
789
+ expireSession();
790
+ const message = { ...sigMessage(1), round: 1, p2pMessages: [{ payload: { encryptedMessage: 'encryptie', signature: 'siggie' } }] };
791
+ const res = await post(path, { message }, partyAuth(1));
792
+ expectError(res, 400, 'TSS_SESSION_EXPIRED');
793
+ });
546
794
  });
547
- it('should throw a mismatch error for a mismatched signature: s', function (done) {
548
- const modifiedSignature = {
549
- ...signature,
550
- s: '0x18048c9ce4a860f6354b4bb795178ff3cde858ed3afef6c2e2c51c8fac8b9256',
551
- };
552
- const xSignature = signRequest({ signature: modifiedSignature });
553
- app.post(url('test'))
554
- .set('x-identity', publicKey)
555
- .set('x-signature', xSignature)
556
- .send({
557
- signature: modifiedSignature
558
- })
559
- .expect(res => {
560
- res.status.should.equal(400);
561
- res.body.should.have.property('code');
562
- res.body.code.should.equal('TSS_FINAL_SIGNATURE_MISMATCH');
563
- res.body.message.should.equal('Final signature does not match the one generated by the session');
564
- })
565
- .end(done);
795
+ describe('GET /v1/tss/sign/:id/:round', function () {
796
+ const path = `/v1/tss/sign/${SESSION_ID}/0?maxWaitTime=0`;
797
+ it('should return round messages to a session participant', async function () {
798
+ await createSigSession();
799
+ const res = await get(path, partyAuth(0));
800
+ res.status.should.equal(200);
801
+ const body = JSON.parse(res.text);
802
+ body.messages.length.should.equal(1);
803
+ body.participants.should.deep.equal([copayerIds[1]]);
804
+ });
805
+ it('should error if the copayer is not registered', async function () {
806
+ await createSigSession();
807
+ const res = await get(path, { copayerId: 'unknownCopayer', privKey: randomKey() });
808
+ expectError(res, 401, 'NOT_AUTHORIZED', 'Copayer not found');
809
+ });
810
+ it('should error if the copayer is not a session participant', async function () {
811
+ await createSigSession();
812
+ const res = await get(path, partyAuth(2));
813
+ expectError(res, 401, 'NOT_AUTHORIZED', 'Copayer not found in session');
814
+ });
815
+ it('should error if the request signature is invalid', async function () {
816
+ await createSigSession();
817
+ const res = await get(path, { copayerId: copayerIds[0], privKey: randomKey() });
818
+ expectError(res, 401, 'NOT_AUTHORIZED', 'Invalid signature');
819
+ });
820
+ it('should reject another participant identity', async function () {
821
+ await createSigSession();
822
+ const res = await get(path, { copayerId: copayerIds[1], privKey: parties[0].authKey });
823
+ expectError(res, 401, 'NOT_AUTHORIZED', 'Invalid signature');
824
+ });
825
+ it('should error if the session does not exist', async function () {
826
+ const res = await get(path, partyAuth(0));
827
+ expectError(res, 400, 'TSS_SESSION_NOT_FOUND');
828
+ });
829
+ it('should error if session is expired', async function () {
830
+ await createSigSession({ timeLimit: 1 });
831
+ expireSession();
832
+ const res = await get(path, partyAuth(0));
833
+ expectError(res, 400, 'TSS_SESSION_EXPIRED');
834
+ });
566
835
  });
567
- it('should throw a mismatch error for a mismatched signature: v', function (done) {
568
- const modifiedSignature = {
569
- ...signature,
570
- v: 123,
836
+ describe('POST /v1/tss/sign/:id/store', function () {
837
+ const path = `/v1/tss/sign/${SESSION_ID}/store`;
838
+ const signature = {
839
+ r: '0x71a8fb6b23203f7ae7f5df7dac410204d8b8d8b74e31c3919e0c764a00925f74',
840
+ s: '0x18048c9ce4a860f6354b4bb795178ff3cde858ed3afef6c2e2c51c8fac8b9257',
841
+ v: 1,
842
+ pubKey: '0267c2c572fcc1a5026d0e5d8e60a04ff6adeaf1a3a996c9c69f3b1714cff1db85',
571
843
  };
572
- const xSignature = signRequest({ signature: modifiedSignature });
573
- app.post(url('test'))
574
- .set('x-identity', publicKey)
575
- .set('x-signature', xSignature)
576
- .send({
577
- signature: modifiedSignature
578
- })
579
- .expect(res => {
580
- res.status.should.equal(400);
581
- res.body.should.have.property('code');
582
- res.body.code.should.equal('TSS_FINAL_SIGNATURE_MISMATCH');
583
- res.body.message.should.equal('Final signature does not match the one generated by the session');
584
- })
585
- .end(done);
586
- });
587
- it('should throw a mismatch error for a mismatched signature: pubKey', function (done) {
588
- const modifiedSignature = {
589
- ...signature,
590
- pubKey: 'not-equal-pub-key',
844
+ const expectMismatch = res => expectError(res, 400, 'TSS_FINAL_SIGNATURE_MISMATCH', 'Final signature does not match the one generated by the session');
845
+ const stubStoreRaceCondition = () => {
846
+ const fetchSessionStub = sandbox.stub(index_1.default.Storage.prototype, 'fetchTssSigSession');
847
+ fetchSessionStub.callsFake(async function ({ id }) {
848
+ const session = await fetchSessionStub.wrappedMethod.call(this, { id });
849
+ session.signature = null;
850
+ return session;
851
+ });
852
+ fetchSessionStub.onThirdCall().callsFake(async function ({ id }) {
853
+ return fetchSessionStub.wrappedMethod.call(this, { id });
854
+ });
591
855
  };
592
- const xSignature = signRequest({ signature: modifiedSignature });
593
- app.post(url('test'))
594
- .set('x-identity', publicKey)
595
- .set('x-signature', xSignature)
596
- .send({
597
- signature: modifiedSignature
598
- })
599
- .expect(res => {
600
- res.status.should.equal(400);
601
- res.body.should.have.property('code');
602
- res.body.code.should.equal('TSS_FINAL_SIGNATURE_MISMATCH');
603
- res.body.message.should.equal('Final signature does not match the one generated by the session');
604
- })
605
- .end(done);
606
- });
607
- it('should cleanly handle race condition when saving signature', function (done) {
608
- const fetchSessionStub = sandbox.stub(index_1.default.Storage.prototype, 'fetchTssSigSession');
609
- fetchSessionStub.callsFake(async function ({ id }) {
610
- const session = await fetchSessionStub.wrappedMethod.call(this, { id });
611
- session.signature = null;
612
- return session;
613
- });
614
- fetchSessionStub.onThirdCall().callsFake(async function ({ id }) {
615
- const session = await fetchSessionStub.wrappedMethod.call(this, { id });
616
- return session;
617
- });
618
- const xSignature = signRequest({ signature });
619
- app.post(url('test'))
620
- .set('x-identity', publicKey)
621
- .set('x-signature', xSignature)
622
- .send({
623
- signature
624
- })
625
- .expect(res => {
856
+ beforeEach(async function () {
857
+ await createSigSession();
858
+ await storage.storeTssSignature({ id: SESSION_ID, signature });
859
+ });
860
+ it('should accept a signature matching the session signature', async function () {
861
+ const res = await post(path, { signature }, partyAuth(1));
626
862
  res.status.should.equal(200);
627
- })
628
- .end(done);
629
- });
630
- it('should handle race condition when saving signature but mismatched signature', function (done) {
631
- const fetchSessionStub = sandbox.stub(index_1.default.Storage.prototype, 'fetchTssSigSession');
632
- fetchSessionStub.callsFake(async function ({ id }) {
633
- const session = await fetchSessionStub.wrappedMethod.call(this, { id });
634
- session.signature = null;
635
- return session;
636
- });
637
- fetchSessionStub.onThirdCall().callsFake(async function ({ id }) {
638
- const session = await fetchSessionStub.wrappedMethod.call(this, { id });
639
- return session;
640
- });
641
- const modifiedSignature = {
642
- ...signature,
643
- r: '0x71a8fb6b23203f7ae7f5df7dac410204d8b8d8b74e31c3919e0c764a00925f75',
644
- };
645
- const xSignature = signRequest({ signature: modifiedSignature });
646
- app.post(url('test'))
647
- .set('x-identity', publicKey)
648
- .set('x-signature', xSignature)
649
- .send({
650
- signature: modifiedSignature
651
- })
652
- .expect(res => {
653
- res.status.should.equal(400);
654
- res.body.should.have.property('code');
655
- res.body.code.should.equal('TSS_FINAL_SIGNATURE_MISMATCH');
656
- res.body.message.should.equal('Final signature does not match the one generated by the session');
657
- })
658
- .end(done);
863
+ });
864
+ for (const [field, value] of [
865
+ ['r', '0x71a8fb6b23203f7ae7f5df7dac410204d8b8d8b74e31c3919e0c764a00925f75'],
866
+ ['s', '0x18048c9ce4a860f6354b4bb795178ff3cde858ed3afef6c2e2c51c8fac8b9256'],
867
+ ['v', 123],
868
+ ['pubKey', 'not-equal-pub-key'],
869
+ ]) {
870
+ it(`should throw a mismatch error for a mismatched signature: ${field}`, async function () {
871
+ const res = await post(path, { signature: { ...signature, [field]: value } }, partyAuth(0));
872
+ expectMismatch(res);
873
+ });
874
+ }
875
+ it('should cleanly handle race condition when saving signature', async function () {
876
+ stubStoreRaceCondition();
877
+ const res = await post(path, { signature }, partyAuth(0));
878
+ res.status.should.equal(200);
879
+ });
880
+ it('should handle race condition when saving signature but mismatched signature', async function () {
881
+ stubStoreRaceCondition();
882
+ const modifiedSignature = {
883
+ ...signature,
884
+ r: '0x71a8fb6b23203f7ae7f5df7dac410204d8b8d8b74e31c3919e0c764a00925f75',
885
+ };
886
+ const res = await post(path, { signature: modifiedSignature }, partyAuth(0));
887
+ expectMismatch(res);
888
+ });
889
+ it('should error if the copayer is not registered', async function () {
890
+ const res = await post(path, { signature }, { copayerId: 'unknownCopayer', privKey: randomKey() });
891
+ expectError(res, 401, 'NOT_AUTHORIZED', 'Copayer not found');
892
+ });
893
+ it('should error if the copayer is not a session participant', async function () {
894
+ const res = await post(path, { signature }, partyAuth(2));
895
+ expectError(res, 401, 'NOT_AUTHORIZED', 'Copayer not found in session');
896
+ });
897
+ it('should error if the request signature is invalid', async function () {
898
+ const res = await post(path, { signature }, { copayerId: copayerIds[0], privKey: randomKey() });
899
+ expectError(res, 401, 'NOT_AUTHORIZED', 'Invalid signature');
900
+ });
659
901
  });
660
902
  });
661
903
  });