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

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 (155) hide show
  1. package/package.json +5 -5
  2. package/src/config.ts +0 -20
  3. package/src/externalservices/banxa.ts +51 -10
  4. package/src/externalservices/changelly.ts +54 -6
  5. package/src/externalservices/moonpay.ts +85 -19
  6. package/src/externalservices/oneInch.ts +13 -5
  7. package/src/externalservices/ramp.ts +42 -8
  8. package/src/externalservices/sardine.ts +51 -10
  9. package/src/externalservices/simplex.ts +58 -11
  10. package/src/externalservices/thorswap.ts +46 -11
  11. package/src/externalservices/transak.ts +53 -11
  12. package/src/lib/common/constants.ts +1 -1
  13. package/src/lib/errors/errordefinitions.ts +8 -0
  14. package/src/lib/model/tsskeygen.ts +12 -4
  15. package/src/lib/routes/middleware/authRequest.ts +6 -16
  16. package/src/lib/routes/middleware/authTssRequest.ts +130 -26
  17. package/src/lib/routes/services.ts +0 -12
  18. package/src/lib/routes/tss.ts +21 -8
  19. package/src/lib/routes/wallets.ts +8 -9
  20. package/src/lib/server.ts +9 -20
  21. package/src/lib/storage.ts +55 -22
  22. package/src/lib/tss.ts +77 -1
  23. package/src/types/externalservices.d.ts +0 -1
  24. package/test/integration/externalservices/banxa.test.ts +95 -5
  25. package/test/integration/externalservices/changelly.test.ts +118 -6
  26. package/test/integration/externalservices/coinGecko.test.ts +4 -0
  27. package/test/integration/externalservices/moonpay.test.ts +178 -9
  28. package/test/integration/externalservices/oneInch.test.ts +28 -1
  29. package/test/integration/externalservices/ramp.test.ts +126 -4
  30. package/test/integration/externalservices/sardine.test.ts +95 -5
  31. package/test/integration/externalservices/simplex.test.ts +95 -5
  32. package/test/integration/externalservices/thorswap.test.ts +81 -5
  33. package/test/integration/externalservices/transak.test.ts +98 -6
  34. package/test/integration/helpers.ts +1 -1
  35. package/test/integration/server.test.ts +56 -88
  36. package/test/integration/tss.test.ts +706 -371
  37. package/test/storage.test.ts +6 -7
  38. package/ts_build/package.json +4 -4
  39. package/ts_build/src/config.d.ts.map +1 -1
  40. package/ts_build/src/config.js +0 -4
  41. package/ts_build/src/config.js.map +1 -1
  42. package/ts_build/src/externalservices/banxa.d.ts.map +1 -1
  43. package/ts_build/src/externalservices/banxa.js +51 -10
  44. package/ts_build/src/externalservices/banxa.js.map +1 -1
  45. package/ts_build/src/externalservices/changelly.d.ts.map +1 -1
  46. package/ts_build/src/externalservices/changelly.js +54 -6
  47. package/ts_build/src/externalservices/changelly.js.map +1 -1
  48. package/ts_build/src/externalservices/moonpay.d.ts.map +1 -1
  49. package/ts_build/src/externalservices/moonpay.js +85 -19
  50. package/ts_build/src/externalservices/moonpay.js.map +1 -1
  51. package/ts_build/src/externalservices/oneInch.d.ts.map +1 -1
  52. package/ts_build/src/externalservices/oneInch.js +13 -5
  53. package/ts_build/src/externalservices/oneInch.js.map +1 -1
  54. package/ts_build/src/externalservices/ramp.d.ts.map +1 -1
  55. package/ts_build/src/externalservices/ramp.js +42 -8
  56. package/ts_build/src/externalservices/ramp.js.map +1 -1
  57. package/ts_build/src/externalservices/sardine.d.ts.map +1 -1
  58. package/ts_build/src/externalservices/sardine.js +51 -10
  59. package/ts_build/src/externalservices/sardine.js.map +1 -1
  60. package/ts_build/src/externalservices/simplex.d.ts.map +1 -1
  61. package/ts_build/src/externalservices/simplex.js +55 -11
  62. package/ts_build/src/externalservices/simplex.js.map +1 -1
  63. package/ts_build/src/externalservices/thorswap.d.ts.map +1 -1
  64. package/ts_build/src/externalservices/thorswap.js +45 -11
  65. package/ts_build/src/externalservices/thorswap.js.map +1 -1
  66. package/ts_build/src/externalservices/transak.d.ts.map +1 -1
  67. package/ts_build/src/externalservices/transak.js +53 -11
  68. package/ts_build/src/externalservices/transak.js.map +1 -1
  69. package/ts_build/src/lib/common/constants.d.ts +1 -1
  70. package/ts_build/src/lib/common/constants.js +1 -1
  71. package/ts_build/src/lib/common/index.d.ts +1 -1
  72. package/ts_build/src/lib/errors/errordefinitions.d.ts +4 -0
  73. package/ts_build/src/lib/errors/errordefinitions.d.ts.map +1 -1
  74. package/ts_build/src/lib/errors/errordefinitions.js +4 -0
  75. package/ts_build/src/lib/errors/errordefinitions.js.map +1 -1
  76. package/ts_build/src/lib/model/tsskeygen.d.ts +2 -0
  77. package/ts_build/src/lib/model/tsskeygen.d.ts.map +1 -1
  78. package/ts_build/src/lib/model/tsskeygen.js +6 -2
  79. package/ts_build/src/lib/model/tsskeygen.js.map +1 -1
  80. package/ts_build/src/lib/routes/middleware/authRequest.d.ts.map +1 -1
  81. package/ts_build/src/lib/routes/middleware/authRequest.js +6 -16
  82. package/ts_build/src/lib/routes/middleware/authRequest.js.map +1 -1
  83. package/ts_build/src/lib/routes/middleware/authTssRequest.d.ts +2 -1
  84. package/ts_build/src/lib/routes/middleware/authTssRequest.d.ts.map +1 -1
  85. package/ts_build/src/lib/routes/middleware/authTssRequest.js +104 -24
  86. package/ts_build/src/lib/routes/middleware/authTssRequest.js.map +1 -1
  87. package/ts_build/src/lib/routes/services.d.ts.map +1 -1
  88. package/ts_build/src/lib/routes/services.js +0 -10
  89. package/ts_build/src/lib/routes/services.js.map +1 -1
  90. package/ts_build/src/lib/routes/tss.d.ts.map +1 -1
  91. package/ts_build/src/lib/routes/tss.js +19 -7
  92. package/ts_build/src/lib/routes/tss.js.map +1 -1
  93. package/ts_build/src/lib/routes/wallets.d.ts.map +1 -1
  94. package/ts_build/src/lib/routes/wallets.js +8 -9
  95. package/ts_build/src/lib/routes/wallets.js.map +1 -1
  96. package/ts_build/src/lib/server.d.ts +0 -2
  97. package/ts_build/src/lib/server.d.ts.map +1 -1
  98. package/ts_build/src/lib/server.js +8 -20
  99. package/ts_build/src/lib/server.js.map +1 -1
  100. package/ts_build/src/lib/storage.d.ts +9 -2
  101. package/ts_build/src/lib/storage.d.ts.map +1 -1
  102. package/ts_build/src/lib/storage.js +51 -20
  103. package/ts_build/src/lib/storage.js.map +1 -1
  104. package/ts_build/src/lib/tss.d.ts +8 -0
  105. package/ts_build/src/lib/tss.d.ts.map +1 -1
  106. package/ts_build/src/lib/tss.js +59 -1
  107. package/ts_build/src/lib/tss.js.map +1 -1
  108. package/ts_build/test/integration/externalservices/banxa.test.d.ts.map +1 -1
  109. package/ts_build/test/integration/externalservices/banxa.test.js +93 -5
  110. package/ts_build/test/integration/externalservices/banxa.test.js.map +1 -1
  111. package/ts_build/test/integration/externalservices/changelly.test.d.ts.map +1 -1
  112. package/ts_build/test/integration/externalservices/changelly.test.js +111 -6
  113. package/ts_build/test/integration/externalservices/changelly.test.js.map +1 -1
  114. package/ts_build/test/integration/externalservices/coinGecko.test.js +4 -0
  115. package/ts_build/test/integration/externalservices/coinGecko.test.js.map +1 -1
  116. package/ts_build/test/integration/externalservices/moonpay.test.d.ts.map +1 -1
  117. package/ts_build/test/integration/externalservices/moonpay.test.js +169 -9
  118. package/ts_build/test/integration/externalservices/moonpay.test.js.map +1 -1
  119. package/ts_build/test/integration/externalservices/oneInch.test.d.ts.map +1 -1
  120. package/ts_build/test/integration/externalservices/oneInch.test.js +26 -1
  121. package/ts_build/test/integration/externalservices/oneInch.test.js.map +1 -1
  122. package/ts_build/test/integration/externalservices/ramp.test.d.ts.map +1 -1
  123. package/ts_build/test/integration/externalservices/ramp.test.js +115 -4
  124. package/ts_build/test/integration/externalservices/ramp.test.js.map +1 -1
  125. package/ts_build/test/integration/externalservices/sardine.test.d.ts.map +1 -1
  126. package/ts_build/test/integration/externalservices/sardine.test.js +94 -5
  127. package/ts_build/test/integration/externalservices/sardine.test.js.map +1 -1
  128. package/ts_build/test/integration/externalservices/simplex.test.d.ts.map +1 -1
  129. package/ts_build/test/integration/externalservices/simplex.test.js +89 -5
  130. package/ts_build/test/integration/externalservices/simplex.test.js.map +1 -1
  131. package/ts_build/test/integration/externalservices/thorswap.test.d.ts.map +1 -1
  132. package/ts_build/test/integration/externalservices/thorswap.test.js +76 -5
  133. package/ts_build/test/integration/externalservices/thorswap.test.js.map +1 -1
  134. package/ts_build/test/integration/externalservices/transak.test.d.ts.map +1 -1
  135. package/ts_build/test/integration/externalservices/transak.test.js +93 -6
  136. package/ts_build/test/integration/externalservices/transak.test.js.map +1 -1
  137. package/ts_build/test/integration/helpers.js +1 -1
  138. package/ts_build/test/integration/helpers.js.map +1 -1
  139. package/ts_build/test/integration/server.test.js +44 -76
  140. package/ts_build/test/integration/server.test.js.map +1 -1
  141. package/ts_build/test/integration/tss.test.d.ts.map +1 -1
  142. package/ts_build/test/integration/tss.test.js +599 -357
  143. package/ts_build/test/integration/tss.test.js.map +1 -1
  144. package/ts_build/test/storage.test.js +6 -7
  145. package/ts_build/test/storage.test.js.map +1 -1
  146. package/src/externalservices/wyre.ts +0 -131
  147. package/test/integration/externalservices/wyre.test.ts +0 -270
  148. package/ts_build/src/externalservices/wyre.d.ts +0 -7
  149. package/ts_build/src/externalservices/wyre.d.ts.map +0 -1
  150. package/ts_build/src/externalservices/wyre.js +0 -144
  151. package/ts_build/src/externalservices/wyre.js.map +0 -1
  152. package/ts_build/test/integration/externalservices/wyre.test.d.ts +0 -2
  153. package/ts_build/test/integration/externalservices/wyre.test.d.ts.map +0 -1
  154. package/ts_build/test/integration/externalservices/wyre.test.js +0 -281
  155. package/ts_build/test/integration/externalservices/wyre.test.js.map +0 -1
@@ -1,5 +1,4 @@
1
1
  import sinon from 'sinon';
2
- import * as chai from 'chai';
3
2
  import 'chai/register-should';
4
3
  import { BitcoreLib } from '@bitpay-labs/crypto-wallet-core';
5
4
  import request from 'supertest';
@@ -11,9 +10,21 @@ import { TssSigGenModel } from '../../src/lib/model/tsssign';
11
10
  import { TssKeyGenModel } from '../../src/lib/model/tsskeygen';
12
11
  import { Common } from '../../src/lib/common/index';
13
12
 
14
- const should = chai.should();
15
13
  const { Utils } = Common;
16
14
 
15
+ function xPubToCopayerId(chain: string, xpub: string): string {
16
+ chain = chain.toLowerCase();
17
+ const str = chain == 'btc' ? xpub : (chain + xpub);
18
+ const hash = BitcoreLib.crypto.Hash.sha256(Buffer.from(str));
19
+ return hash.toString('hex');
20
+ }
21
+
22
+ /** Each party has its own copayer ID (derived from its own seed, not the shared TSS key) */
23
+ function getXPubKey(seed: Buffer, network = 'testnet'): string {
24
+ return BitcoreLib.HDPrivateKey.fromSeed(seed, network).hdPublicKey.toString();
25
+ }
26
+
27
+
17
28
  describe('TSS', function() {
18
29
  this.timeout(Math.max(this['_timeout'], 2000));
19
30
 
@@ -42,6 +53,8 @@ describe('TSS', function() {
42
53
  messages: {
43
54
  round0: {
44
55
  party0: {
56
+ round: 0,
57
+ partyId: 0,
45
58
  p2pMessages: [],
46
59
  broadcastMessages: [{
47
60
  from: 0,
@@ -53,6 +66,8 @@ describe('TSS', function() {
53
66
  }]
54
67
  },
55
68
  party1: {
69
+ round: 0,
70
+ partyId: 1,
56
71
  p2pMessages: [],
57
72
  broadcastMessages: [{
58
73
  from: 1,
@@ -64,6 +79,8 @@ describe('TSS', function() {
64
79
  }]
65
80
  },
66
81
  party2: {
82
+ round: 0,
83
+ partyId: 2,
67
84
  p2pMessages: [],
68
85
  broadcastMessages: [{
69
86
  from: 2,
@@ -77,6 +94,8 @@ describe('TSS', function() {
77
94
  },
78
95
  round1: {
79
96
  party0: {
97
+ round: 1,
98
+ partyId: 0,
80
99
  p2pMessages: [{
81
100
  from: 0,
82
101
  to: 1,
@@ -91,6 +110,8 @@ describe('TSS', function() {
91
110
  broadcastMessages: []
92
111
  },
93
112
  party1: {
113
+ round: 1,
114
+ partyId: 1,
94
115
  p2pMessages: [{
95
116
  from: 1,
96
117
  to: 0,
@@ -105,6 +126,8 @@ describe('TSS', function() {
105
126
  broadcastMessages: []
106
127
  },
107
128
  party2: {
129
+ round: 1,
130
+ partyId: 2,
108
131
  p2pMessages: [{
109
132
  from: 2,
110
133
  to: 0,
@@ -121,6 +144,8 @@ describe('TSS', function() {
121
144
  },
122
145
  round2: {
123
146
  party0: {
147
+ round: 2,
148
+ partyId: 0,
124
149
  p2pMessages: [{
125
150
  from: 0,
126
151
  to: 1,
@@ -135,6 +160,8 @@ describe('TSS', function() {
135
160
  broadcastMessages: []
136
161
  },
137
162
  party1: {
163
+ round: 2,
164
+ partyId: 1,
138
165
  p2pMessages: [{
139
166
  from: 1,
140
167
  to: 0,
@@ -149,6 +176,8 @@ describe('TSS', function() {
149
176
  broadcastMessages: []
150
177
  },
151
178
  party2: {
179
+ round: 2,
180
+ partyId: 2,
152
181
  p2pMessages: [{
153
182
  from: 2,
154
183
  to: 0,
@@ -165,6 +194,8 @@ describe('TSS', function() {
165
194
  },
166
195
  round3: {
167
196
  party0: {
197
+ round: 3,
198
+ partyId: 0,
168
199
  p2pMessages: [],
169
200
  broadcastMessages: [
170
201
  {
@@ -178,6 +209,8 @@ describe('TSS', function() {
178
209
  ],
179
210
  },
180
211
  party1: {
212
+ round: 3,
213
+ partyId: 1,
181
214
  p2pMessages: [],
182
215
  broadcastMessages: [
183
216
  {
@@ -191,6 +224,8 @@ describe('TSS', function() {
191
224
  ],
192
225
  },
193
226
  party2: {
227
+ round: 3,
228
+ partyId: 2,
194
229
  p2pMessages: [],
195
230
  broadcastMessages: [
196
231
  {
@@ -214,12 +249,97 @@ describe('TSS', function() {
214
249
  messageHash: '516ab037171ee1a7787cbe07c28948dd0252ea60b7f40ee6ffbb7e1271f691ea'
215
250
  }]
216
251
  };
217
- const n = 2;
218
- const m = 3;
219
252
 
220
253
  let app;
254
+ let storage;
221
255
  const urlPrefix = '/bws/api';
222
256
  const sandbox = sinon.createSandbox();
257
+ const SESSION_ID = 'test';
258
+ const copayer1 = xPubToCopayerId('btc', getXPubKey(vector.party0.seed));
259
+ const copayer2 = xPubToCopayerId('btc', getXPubKey(vector.party1.seed));
260
+ const copayer3 = xPubToCopayerId('btc', getXPubKey(vector.party2.seed));
261
+
262
+ const parties = [vector.party0, vector.party1, vector.party2];
263
+ const copayerIds = [copayer1, copayer2, copayer3];
264
+ const { round0, round1 } = vector.keygen.messages;
265
+ const round0Messages = [round0.party0, round0.party1, round0.party2];
266
+ const round1Messages = [round1.party0, round1.party1, round1.party2];
267
+
268
+ type Auth = { copayerId: string; privKey: BitcoreLib.PrivateKey };
269
+
270
+ const authPubKey = (partyId: number): string => parties[partyId].authKey.publicKey.toString();
271
+ const partyAuth = (partyId: number): Auth => ({ copayerId: copayerIds[partyId], privKey: parties[partyId].authKey });
272
+ const randomKey = () => new BitcoreLib.PrivateKey();
273
+
274
+ const keyGenMessage = (messages: any[], partyId: number) => ({
275
+ ...JSON.parse(JSON.stringify(messages[partyId])),
276
+ publicKey: authPubKey(partyId)
277
+ });
278
+ const round0Message = (partyId: number) => keyGenMessage(round0Messages, partyId);
279
+ const round1Message = (partyId: number) => keyGenMessage(round1Messages, partyId);
280
+
281
+ /** Sign a request the same way BWC does: hash with reversal, then sign little-endian */
282
+ const signRequest = (method: string, path: string, body: object, privKey: BitcoreLib.PrivateKey): string => {
283
+ const hash = Utils.hashMessage(`${method}|${path}|${JSON.stringify(body)}`, false);
284
+ return BitcoreLib.crypto.ECDSA.sign(hash, privKey, { endian: 'little' }).toString();
285
+ };
286
+
287
+ const post = (path: string, body: object, auth: Auth) =>
288
+ app.post(urlPrefix + path)
289
+ .set('x-identity', auth.copayerId)
290
+ .set('x-signature', signRequest('post', path, body, auth.privKey))
291
+ .send(body);
292
+
293
+ const get = (path: string, auth: Auth) =>
294
+ app.get(urlPrefix + path)
295
+ .set('x-identity', auth.copayerId)
296
+ .set('x-signature', signRequest('get', path, {}, auth.privKey));
297
+
298
+ const expectError = (res, status: number, code: string, message?: string) => {
299
+ res.status.should.equal(status);
300
+ res.body.code.should.equal(code);
301
+ if (message) {
302
+ res.body.message.should.equal(message);
303
+ }
304
+ };
305
+
306
+ /** Advance Date.now() past a 1 minute session time limit */
307
+ const expireSession = () => {
308
+ const now = Date.now() + 2 * 60 * 1000;
309
+ sandbox.stub(Date, 'now').returns(now);
310
+ };
311
+
312
+ /**
313
+ * Store a keygen session initiated by party 0 directly in the db.
314
+ * - `registered`: parties whose auth pub key was registered by the initiator (v1.2+)
315
+ * - `joined`: parties that have also submitted their round 0 message
316
+ */
317
+ const createKeyGenSession = async (opts: { version?: number; registered?: number[]; joined?: number[]; timeLimit?: number } = {}) => {
318
+ const { version = 1.2, registered = [], joined = [], timeLimit } = opts;
319
+ const doc = TssKeyGenModel.create({
320
+ id: SESSION_ID,
321
+ message: round0Message(0),
322
+ n: vector.n,
323
+ copayerId: copayerIds[0],
324
+ version,
325
+ timeLimit
326
+ });
327
+ if (version <= 1.1) {
328
+ // authPublicKeys did not exist before v1.2. Make sure the legacy path doesn't rely on it.
329
+ delete doc.authPublicKeys;
330
+ }
331
+ for (const partyId of registered) {
332
+ doc.authPublicKeys[partyId] = authPubKey(partyId);
333
+ }
334
+ for (const partyId of joined) {
335
+ if (version > 1.1) {
336
+ doc.authPublicKeys[partyId] = authPubKey(partyId);
337
+ }
338
+ doc.participants[partyId] = copayerIds[partyId];
339
+ doc.rounds[0].push({ fromPartyId: partyId, messages: round0Message(partyId) });
340
+ }
341
+ await storage.storeTssKeyGenSession({ doc });
342
+ };
223
343
 
224
344
  before(async function() {
225
345
  await helpers.before();
@@ -230,7 +350,7 @@ describe('TSS', function() {
230
350
  });
231
351
 
232
352
  beforeEach(function(done) {
233
- const storage = helpers.getStorage();
353
+ storage = helpers.getStorage();
234
354
  const blockchainExplorerMock = helpers.getBlockchainExplorer();
235
355
 
236
356
  const expressApp = new BWS.ExpressApp();
@@ -253,430 +373,645 @@ describe('TSS', function() {
253
373
  sandbox.restore();
254
374
  });
255
375
 
256
- describe('POST /v1/tss/keygen/:id', function() {
257
- const url = id => `${urlPrefix}/v1/tss/keygen/${id}`;
258
-
376
+ describe('Key generation', function() {
259
377
  beforeEach(async function() {
260
- const storage = helpers.getStorage();
261
378
  await storage.db.collection(BWS.Storage.collections.TSS_KEYGEN).deleteMany({});
262
379
  });
263
380
 
264
- describe('middleware', function() {
265
- beforeEach(function() {
266
- sandbox.stub(TssKeyGen, 'processMessage').resolves();
267
- });
381
+ describe('POST /v1/tss/keygen/:id', function() {
382
+ const path = `/v1/tss/keygen/${SESSION_ID}`;
268
383
 
269
- it('should verify tss broadcast message', function(done) {
270
- const message = JSON.parse(JSON.stringify(vector.keygen.messages.round0.party0));
271
- message.publicKey = vector.party0.authKey.publicKey.toString();
272
- app.post(url('test'))
273
- .send({ message })
274
- .expect(200)
275
- .end((err, res) => {
276
- should.not.exist(err);
277
- done();
278
- });
384
+ describe('initiating a session', function() {
385
+ it('should create a new session from party 0\'s round 0 message', async function() {
386
+ const res = await post(path, { message: round0Message(0), n: vector.n, version: 1.2 }, partyAuth(0));
387
+ res.status.should.equal(200);
388
+
389
+ const session = await storage.fetchTssKeyGenSession({ id: SESSION_ID });
390
+ session.schemeVersion.should.equal(1.2);
391
+ session.participants.should.deep.equal([copayerIds[0], null, null]);
392
+ session.authPublicKeys.should.deep.equal([authPubKey(0), null, null]);
393
+ });
394
+
395
+ it('should error if x-identity is missing', async function() {
396
+ const res = await app.post(urlPrefix + path).send({ message: round0Message(0), n: vector.n, version: 1.2 });
397
+ expectError(res, 401, 'NOT_AUTHORIZED');
398
+ });
399
+
400
+ it('should error if a party other than 0 tries to start the session', async function() {
401
+ const res = await post(path, { message: round0Message(1), n: vector.n, version: 1.2 }, partyAuth(1));
402
+ expectError(res, 401, 'NOT_AUTHORIZED', 'Session not found');
403
+ });
404
+
405
+ it('should error if the request is not signed by the message\'s publicKey', async function() {
406
+ const res = await post(path, { message: round0Message(0), n: vector.n, version: 1.2 }, { copayerId: copayerIds[0], privKey: randomKey() });
407
+ expectError(res, 401, 'NOT_AUTHORIZED', 'Invalid signature');
408
+ });
409
+
410
+ it('should error if the message has no publicKey', async function() {
411
+ const message = round0Message(0);
412
+ delete message.publicKey;
413
+ const res = await post(path, { message, n: vector.n, version: 1.2 }, partyAuth(0));
414
+ expectError(res, 401, 'NOT_AUTHORIZED');
415
+ });
416
+
417
+ it('should error if party 0 tries to start the session with a non-round 0 message', async function() {
418
+ const res = await post(path, { message: round1Message(0), n: vector.n, version: 1.2 }, partyAuth(0));
419
+ expectError(res, 401, 'NOT_AUTHORIZED', 'Session not found');
420
+ });
279
421
  });
280
422
 
281
- it('should verify tss p2p message', function(done) {
282
- const message = JSON.parse(JSON.stringify(vector.keygen.messages.round1.party0));
283
- message.publicKey = vector.party0.authKey.publicKey.toString();
284
- app.post(url('test'))
285
- .send({ message })
286
- .expect(200)
287
- .end((err, res) => {
288
- should.not.exist(err);
289
- done();
290
- });
423
+ describe('message verification', function() {
424
+ beforeEach(function() {
425
+ sandbox.stub(TssKeyGen, 'processMessage').resolves();
426
+ });
427
+
428
+ it('should verify tss broadcast message', async function() {
429
+ const res = await post(path, { message: round0Message(0) }, partyAuth(0));
430
+ res.status.should.equal(200);
431
+ });
432
+
433
+ it('should verify tss p2p message', async function() {
434
+ await createKeyGenSession({ joined: [1, 2] });
435
+ const res = await post(path, { message: round1Message(0) }, partyAuth(0));
436
+ res.status.should.equal(200);
437
+ });
438
+
439
+ // For an existing session, request auth uses the session's authPublicKeys rather than
440
+ // message.publicKey, so these reach verifyTssMessage instead of failing auth first.
441
+
442
+ it('should error if no publicKey', async function() {
443
+ await createKeyGenSession();
444
+ const message = round0Message(0);
445
+ delete message.publicKey;
446
+ const res = await post(path, { message }, partyAuth(0));
447
+ expectError(res, 400, 'TSS_PUBKEY_MISSING');
448
+ });
449
+
450
+ it('should error if publicKey is bogus', async function() {
451
+ await createKeyGenSession();
452
+ const message = { ...round0Message(0), publicKey: 'invalid' };
453
+ const res = await post(path, { message }, partyAuth(0));
454
+ expectError(res, 400, 'TSS_INVALID_MESSAGE');
455
+ });
456
+
457
+ it('should error if publicKey cannot verify broadcast message signature', async function() {
458
+ await createKeyGenSession();
459
+ const message = { ...round0Message(0), publicKey: randomKey().publicKey.toString() };
460
+ const res = await post(path, { message }, partyAuth(0));
461
+ expectError(res, 400, 'TSS_INVALID_MESSAGE_SIG');
462
+ });
463
+
464
+ it('should error if messages are empty arrays', async function() {
465
+ const message = {
466
+ round: 0,
467
+ partyId: 0,
468
+ broadcastMessages: [],
469
+ p2pMessages: [],
470
+ publicKey: authPubKey(0)
471
+ };
472
+ const res = await post(path, { message }, partyAuth(0));
473
+ expectError(res, 400, 'TSS_INVALID_MESSAGE');
474
+ });
291
475
  });
292
476
 
293
- it('should error if no publicKey', function(done) {
294
- app.post(url('test'))
295
- .send({})
296
- .expect(200)
297
- .end((err, res) => {
298
- should.exist(err);
299
- res.status.should.equal(400);
300
- res.body.code.should.equal('TSS_PUBKEY_MISSING');
301
- done();
302
- });
477
+ describe('joining a v1.2 session', function() {
478
+ it('should let a registered party join', async function() {
479
+ await createKeyGenSession({ registered: [1] });
480
+
481
+ const res = await post(path, { message: round0Message(1), version: 1.2 }, partyAuth(1));
482
+ res.status.should.equal(200);
483
+
484
+ const session = await storage.fetchTssKeyGenSession({ id: SESSION_ID });
485
+ session.participants[1].should.equal(copayerIds[1]);
486
+ session.rounds[0].map(r => r.fromPartyId).should.deep.equal([0, 1]);
487
+ });
488
+
489
+ it('should let registered parties join out of partyId order', async function() {
490
+ await createKeyGenSession({ registered: [1, 2] });
491
+
492
+ for (const partyId of [2, 1]) {
493
+ const res = await post(path, { message: round0Message(partyId), version: 1.2 }, partyAuth(partyId));
494
+ res.status.should.equal(200);
495
+ }
496
+
497
+ const session = await storage.fetchTssKeyGenSession({ id: SESSION_ID });
498
+ session.participants.should.deep.equal(copayerIds);
499
+ });
500
+
501
+ it('should error if the party was not registered by the initiator', async function() {
502
+ await createKeyGenSession({ registered: [1] });
503
+ const res = await post(path, { message: round0Message(2), version: 1.2 }, partyAuth(2));
504
+ expectError(res, 401, 'NOT_AUTHORIZED', 'Not authorized');
505
+ });
506
+
507
+ it('should error if the message has no publicKey', async function() {
508
+ await createKeyGenSession({ registered: [1] });
509
+ const message = round0Message(1);
510
+ delete message.publicKey;
511
+ const res = await post(path, { message, version: 1.2 }, partyAuth(1));
512
+ expectError(res, 401, 'NOT_AUTHORIZED', 'Not authorized');
513
+ });
514
+
515
+ it('should error if the request is not signed by the registered key', async function() {
516
+ await createKeyGenSession({ registered: [1] });
517
+ const res = await post(path, { message: round0Message(1), version: 1.2 }, { copayerId: copayerIds[1], privKey: randomKey() });
518
+ expectError(res, 401, 'NOT_AUTHORIZED', 'Invalid signature');
519
+ });
520
+
521
+ it('should error if the party joins at a partyId other than the one it was registered at', async function() {
522
+ await createKeyGenSession({ registered: [1] });
523
+ const message = { ...round0Message(1), partyId: 2 };
524
+ const res = await post(path, { message, version: 1.2 }, partyAuth(1));
525
+ expectError(res, 401, 'NOT_AUTHORIZED', 'Copayer not found in session');
526
+ });
527
+
528
+ it('should error if the registered party slot has already been taken', async function() {
529
+ await createKeyGenSession({ joined: [1] });
530
+ // A different copayer replaying party 1's join
531
+ const res = await post(path, { message: round0Message(1), version: 1.2 }, { copayerId: copayerIds[2], privKey: parties[1].authKey });
532
+ expectError(res, 401, 'NOT_AUTHORIZED', 'Not authorized');
533
+ });
534
+
535
+ it('should error if a registered party that has not joined sends a non-round 0 message', async function() {
536
+ await createKeyGenSession({ registered: [1] });
537
+ const res = await post(path, { message: round1Message(1), version: 1.2 }, partyAuth(1));
538
+ expectError(res, 401, 'NOT_AUTHORIZED', 'Copayer not found in session');
539
+ });
540
+
541
+ it('should error if session is expired', async function() {
542
+ await createKeyGenSession({ registered: [1], timeLimit: 1 });
543
+ expireSession();
544
+ const res = await post(path, { message: round0Message(1), version: 1.2 }, partyAuth(1));
545
+ expectError(res, 400, 'TSS_SESSION_EXPIRED');
546
+ });
303
547
  });
304
548
 
305
- it('should error if publicKey is bogus', function(done) {
306
- const message = JSON.parse(JSON.stringify(vector.keygen.messages.round0.party0));
307
- message.publicKey = 'invalid';
308
- app.post(url('test'))
309
- .send({ message })
310
- .expect(200)
311
- .end((err, res) => {
312
- should.exist(err);
313
- res.status.should.equal(400);
314
- res.body.code.should.equal('TSS_INVALID_MESSAGE');
315
- done();
316
- });
549
+ describe('joining a legacy (v1.1) session', function() {
550
+ it('should let a party join without being registered', async function() {
551
+ await createKeyGenSession({ version: 1.1 });
552
+
553
+ const res = await post(path, { message: round0Message(1), version: 1.1 }, partyAuth(1));
554
+ res.status.should.equal(200);
555
+
556
+ const session = await storage.fetchTssKeyGenSession({ id: SESSION_ID });
557
+ session.participants[1].should.equal(copayerIds[1]);
558
+ });
559
+
560
+ it('should authenticate a joined party with the publicKey from its round 0 message', async function() {
561
+ sandbox.stub(TssKeyGen, 'processMessage').resolves();
562
+ await createKeyGenSession({ version: 1.1, joined: [1, 2] });
563
+ const res = await post(path, { message: round1Message(1), version: 1.1 }, partyAuth(1));
564
+ res.status.should.equal(200);
565
+ });
566
+
567
+ it('should error if a joined party signs with a key other than its round 0 publicKey', async function() {
568
+ await createKeyGenSession({ version: 1.1, joined: [1, 2] });
569
+ const res = await post(path, { message: round1Message(1), version: 1.1 }, { copayerId: copayerIds[1], privKey: randomKey() });
570
+ expectError(res, 401, 'NOT_AUTHORIZED', 'Invalid signature');
571
+ });
317
572
  });
318
573
 
319
- it('should error if publicKey cannot verify broadcast message signature', function(done) {
320
- const message = JSON.parse(JSON.stringify(vector.keygen.messages.round0.party0));
321
- message.publicKey = new BitcoreLib.PrivateKey().publicKey.toString();
322
- app.post(url('test'))
323
- .send({ message })
324
- .expect(200)
325
- .end((err, res) => {
326
- should.exist(err);
327
- res.status.should.equal(400);
328
- res.body.code.should.equal('TSS_INVALID_MESSAGE_SIG');
329
- done();
574
+ describe('concurrent joins', function() {
575
+ /**
576
+ * Make the first storeTssKeyGenParticipant call lose a race: before it writes,
577
+ * another copayer is written into `concurrentPartyId`, so the original call's __v is stale.
578
+ */
579
+ const stubJoinRaceCondition = (concurrentPartyId: number, concurrentCopayerId: string) => {
580
+ const stub = sandbox.stub(BWS.Storage.prototype, 'storeTssKeyGenParticipant');
581
+ stub.callThrough();
582
+ stub.onFirstCall().callsFake(async function(params) {
583
+ const current = await this.fetchTssKeyGenSession({ id: params.id });
584
+ await stub.wrappedMethod.call(this, { id: params.id, partyId: concurrentPartyId, copayerId: concurrentCopayerId, __v: current.__v });
585
+ return stub.wrappedMethod.call(this, params);
330
586
  });
587
+ return stub;
588
+ };
589
+
590
+ it('should retry and join if another party joined concurrently', async function() {
591
+ await createKeyGenSession({ registered: [1, 2] });
592
+ const stub = stubJoinRaceCondition(2, copayerIds[2]);
593
+
594
+ const res = await post(path, { message: round0Message(1), version: 1.2 }, partyAuth(1));
595
+ res.status.should.equal(200);
596
+ stub.callCount.should.equal(2);
597
+
598
+ const session = await storage.fetchTssKeyGenSession({ id: SESSION_ID });
599
+ session.participants.should.deep.equal(copayerIds);
600
+ session.rounds[0].map(r => r.fromPartyId).should.deep.equal([0, 1]);
601
+ });
602
+
603
+ it('should not join if another copayer took the same partyId concurrently', async function() {
604
+ await createKeyGenSession({ version: 1.1 });
605
+ stubJoinRaceCondition(1, copayerIds[2]);
606
+
607
+ const res = await post(path, { message: round0Message(1), version: 1.1 }, partyAuth(1));
608
+ res.status.should.not.equal(200);
609
+
610
+ const session = await storage.fetchTssKeyGenSession({ id: SESSION_ID });
611
+ session.participants[1].should.equal(copayerIds[2]);
612
+ session.rounds[0].map(r => r.fromPartyId).should.deep.equal([0]);
613
+ });
614
+
615
+ it('should not retry on other storage errors', async function() {
616
+ await createKeyGenSession({ registered: [1] });
617
+ const stub = sandbox.stub(BWS.Storage.prototype, 'storeTssKeyGenParticipant').rejects(new Error('boom'));
618
+
619
+ const res = await post(path, { message: round0Message(1), version: 1.2 }, partyAuth(1));
620
+ res.status.should.equal(500);
621
+ stub.callCount.should.equal(1);
622
+ });
331
623
  });
332
624
 
333
- it('should error if messages are empty arrays', function(done) {
334
- const message = {
335
- broadcastMessages: [],
336
- p2pMessages: [],
337
- publicKey: new BitcoreLib.PrivateKey().publicKey.toString()
338
- };
339
- app.post(url('test'))
340
- .send({ message })
341
- .expect(200)
342
- .end((err, res) => {
343
- should.exist(err);
344
- res.status.should.equal(400);
345
- res.body.code.should.equal('TSS_INVALID_MESSAGE');
346
- done();
347
- });
625
+ describe('existing participants', function() {
626
+ it('should error if the request signature is invalid', async function() {
627
+ await createKeyGenSession({ joined: [1, 2] });
628
+ const res = await post(path, { message: round1Message(0), version: 1.2 }, { copayerId: copayerIds[0], privKey: randomKey() });
629
+ expectError(res, 401, 'NOT_AUTHORIZED', 'Invalid signature');
630
+ });
631
+
632
+ it('should error if session is expired', async function() {
633
+ await createKeyGenSession({ joined: [1, 2], timeLimit: 1 });
634
+ expireSession();
635
+ const res = await post(path, { message: round1Message(0), version: 1.2 }, partyAuth(0));
636
+ expectError(res, 400, 'TSS_SESSION_EXPIRED');
637
+ });
348
638
  });
349
639
  });
350
640
 
351
- it('should error if session is expired', function(done) {
352
- const storage = helpers.getStorage();
353
- const publicKey = vector.party0.authKey.publicKey.toString();
354
- const keyGen = TssKeyGenModel.create({
355
- id: 'test',
356
- message: { partyId: 0, publicKey } as any,
357
- n: 2,
358
- copayerId: publicKey,
359
- version: 1.1,
360
- timeLimit: 1, // 1 minute
361
- });
362
- storage.storeTssKeyGenSession({ doc: keyGen }).then(() => {
363
- // advance time by 2 minutes
364
- const now = Date.now() + 2 * 60 * 1000;
365
- sandbox.stub(Date, 'now').returns(now);
366
-
367
- const message = JSON.parse(JSON.stringify(vector.keygen.messages.round1.party0));
368
- message.publicKey = publicKey;
369
-
370
- app.post(url('test'))
371
- .set('x-identity', publicKey)
372
- .send({ message })
373
- .expect((res) => {
374
- res.status.should.equal(400);
375
- res.body.code.should.equal('TSS_SESSION_EXPIRED');
376
- })
377
- .end(done);
378
- }).catch(done);
379
- });
380
- });
641
+ describe('POST /v1/tss/keygen/:id/register', function() {
642
+ const path = `/v1/tss/keygen/${SESSION_ID}/register`;
643
+ const register = (partyId: number | string, partyPubKey: string, auth: Auth = partyAuth(0)) =>
644
+ post(path, { partyId, partyPubKey }, auth);
381
645
 
382
- describe('GET /v1/tss/keygen/:id/:round', function() {
383
- const roundUrl = '/v1/tss/keygen/test/0?maxWaitTime=0';
384
- const parties = [vector.party0, vector.party1, vector.party2];
385
- const round0Messages = [vector.keygen.messages.round0.party0, vector.keygen.messages.round0.party1, vector.keygen.messages.round0.party2];
386
-
387
- const joinRound0 = async (partyId: number) => {
388
- const publicKey = parties[partyId].authKey.publicKey.toString();
389
- const message = { ...round0Messages[partyId], partyId, round: 0, publicKey };
390
- await app.post(`${urlPrefix}/v1/tss/keygen/test`)
391
- .set('x-identity', publicKey)
392
- .send({ message, n: 3, version: 1.1 })
393
- .expect(200);
394
- };
646
+ it('should let the initiator register a party', async function() {
647
+ await createKeyGenSession();
395
648
 
396
- const getRound0 = (partyId: number) => {
397
- const { authKey } = parties[partyId];
398
- const hash = Utils.hashMessage(`get|${roundUrl}|{}`, false);
399
- const xSignature = BitcoreLib.crypto.ECDSA.sign(hash, authKey, { endian: 'little' }).toString();
400
- return app.get(urlPrefix + roundUrl)
401
- .set('x-identity', authKey.publicKey.toString())
402
- .set('x-signature', xSignature);
403
- };
649
+ const res = await register(1, authPubKey(1));
650
+ res.status.should.equal(200);
404
651
 
405
- beforeEach(async function() {
406
- const storage = helpers.getStorage();
407
- await storage.db.collection(BWS.Storage.collections.TSS_KEYGEN).deleteMany({});
408
- });
652
+ const session = await storage.fetchTssKeyGenSession({ id: SESSION_ID });
653
+ session.authPublicKeys.should.deep.equal([authPubKey(0), authPubKey(1), null]);
654
+ });
409
655
 
410
- it('should authenticate every party when round 0 messages arrive in partyId order', async function() {
411
- await joinRound0(0);
412
- await joinRound0(1);
413
- await joinRound0(2);
656
+ it('should be idempotent when re-registering the same key at the same partyId', async function() {
657
+ await createKeyGenSession();
414
658
 
415
- for (const partyId of [0, 1, 2]) {
416
- const res = await getRound0(partyId);
417
- res.status.should.equal(200);
418
- JSON.parse(res.text).messages.length.should.equal(2);
659
+ (await register(1, authPubKey(1))).status.should.equal(200);
660
+ (await register(1, authPubKey(1))).status.should.equal(200);
661
+
662
+ const session = await storage.fetchTssKeyGenSession({ id: SESSION_ID });
663
+ session.authPublicKeys[1].should.equal(authPubKey(1));
664
+ session.__v.should.equal(1); // second request did not write
665
+ });
666
+
667
+ it('should error if the key is already registered at a different partyId', async function() {
668
+ await createKeyGenSession();
669
+ (await register(1, authPubKey(1))).status.should.equal(200);
670
+
671
+ const res = await register(2, authPubKey(1));
672
+ expectError(res, 400, 'TSS_PARTY_ALREADY_REGISTERED');
673
+ });
674
+
675
+ it('should error if registering the initiator\'s own key', async function() {
676
+ await createKeyGenSession();
677
+ const res = await register(1, authPubKey(0));
678
+ expectError(res, 400, 'TSS_PARTY_ALREADY_REGISTERED');
679
+ });
680
+
681
+ for (const [description, partyId, getPubKey] of [
682
+ ['partyId is 0', 0, () => authPubKey(1)],
683
+ ['partyId is negative', -1, () => authPubKey(1)],
684
+ ['partyId is not a number', 'abc', () => authPubKey(1)],
685
+ ['partyId is >= n', vector.n, () => authPubKey(1)],
686
+ ['partyPubKey is missing', 1, () => undefined],
687
+ ['partyPubKey is blank', 1, () => ' '],
688
+ ['partyPubKey is the initiator\'s copayerId', 1, () => copayerIds[0]],
689
+ ] as Array<[string, number | string, () => string]>) {
690
+ it(`should error if ${description}`, async function() {
691
+ await createKeyGenSession();
692
+ const res = await register(partyId, getPubKey());
693
+ expectError(res, 400, 'TSS_INVALID_PARTY_DATA');
694
+ });
419
695
  }
420
- });
421
696
 
422
- it('should authenticate every party when round 0 messages arrive out of partyId order', async function() {
423
- await joinRound0(0);
424
- await joinRound0(2);
425
- await joinRound0(1);
697
+ it('should accept a numeric string partyId', async function() {
698
+ await createKeyGenSession();
426
699
 
427
- for (const partyId of [0, 1, 2]) {
428
- const res = await getRound0(partyId);
700
+ const res = await register('1', authPubKey(1));
429
701
  res.status.should.equal(200);
430
- JSON.parse(res.text).messages.length.should.equal(2);
702
+
703
+ const session = await storage.fetchTssKeyGenSession({ id: SESSION_ID });
704
+ session.authPublicKeys[1].should.equal(authPubKey(1));
705
+ });
706
+
707
+ for (const [description, partyId, expected] of [
708
+ ['partyId is missing', undefined, 'undefined'],
709
+ ['partyId is null', null, 'null'],
710
+ ['partyId has a numeric prefix', '1abc', '1abc'],
711
+ ] as Array<[string, any, string]>) {
712
+ it(`should error with an invalid partyId message if ${description}`, async function() {
713
+ await createKeyGenSession();
714
+ const res = await register(partyId, authPubKey(1));
715
+ expectError(res, 400, 'TSS_INVALID_PARTY_DATA', `Invalid partyId: ${expected}`);
716
+
717
+ const session = await storage.fetchTssKeyGenSession({ id: SESSION_ID });
718
+ session.authPublicKeys.should.deep.equal([authPubKey(0), null, null]);
719
+ });
431
720
  }
721
+
722
+ it('should error if the party has already submitted its round 0 message', async function() {
723
+ await createKeyGenSession({ joined: [1] });
724
+ const res = await register(1, randomKey().publicKey.toString());
725
+ expectError(res, 400, 'TSS_ROUNDS_ALREADY_STARTED');
726
+ });
727
+
728
+ it('should error if a joined participant other than the initiator tries to register a party', async function() {
729
+ await createKeyGenSession({ joined: [1] });
730
+ const res = await register(2, authPubKey(2), partyAuth(1));
731
+ expectError(res, 400, 'TSS_NON_PARTICIPANT', 'Only the first participant can register a party');
732
+ });
733
+
734
+ it('should error if the requester is not in the session', async function() {
735
+ await createKeyGenSession();
736
+ const res = await register(1, authPubKey(1), partyAuth(1));
737
+ expectError(res, 401, 'NOT_AUTHORIZED', 'Not authorized');
738
+ });
739
+
740
+ it('should error if the request signature is invalid', async function() {
741
+ await createKeyGenSession();
742
+ const res = await register(1, authPubKey(1), { copayerId: copayerIds[0], privKey: randomKey() });
743
+ expectError(res, 401, 'NOT_AUTHORIZED', 'Invalid signature');
744
+ });
745
+
746
+ it('should error if the session does not exist', async function() {
747
+ const res = await register(1, authPubKey(1));
748
+ expectError(res, 401, 'NOT_AUTHORIZED', 'Session not found');
749
+ });
432
750
  });
433
751
 
434
- it('should reject another party identity when round 0 messages arrive out of partyId order', async function() {
435
- await joinRound0(0);
436
- await joinRound0(2);
437
- await joinRound0(1);
438
-
439
- for (const [partyId, otherPartyId] of [[1, 2], [2, 1]]) {
440
- const res = await getRound0(partyId)
441
- .set('x-identity', parties[otherPartyId].authKey.publicKey.toString());
442
- res.status.should.equal(401);
443
- res.body.code.should.equal('NOT_AUTHORIZED');
444
- res.body.message.should.equal('Invalid signature');
445
- }
752
+ describe('GET /v1/tss/keygen/:id/:round', function() {
753
+ const path = `/v1/tss/keygen/${SESSION_ID}/0?maxWaitTime=0`;
754
+
755
+ const expectRound0Messages = async (auth: Auth) => {
756
+ const res = await get(path, auth);
757
+ res.status.should.equal(200);
758
+ JSON.parse(res.text).messages.length.should.equal(vector.n - 1);
759
+ };
760
+
761
+ /** Start, register, and join a v1.2 session through the API */
762
+ const runRound0 = async (joinOrder: number[]) => {
763
+ const keygenPath = `/v1/tss/keygen/${SESSION_ID}`;
764
+ (await post(keygenPath, { message: round0Message(0), n: vector.n, version: 1.2 }, partyAuth(0))).status.should.equal(200);
765
+ for (const partyId of [1, 2]) {
766
+ (await post(`${keygenPath}/register`, { partyId, partyPubKey: authPubKey(partyId) }, partyAuth(0))).status.should.equal(200);
767
+ }
768
+ for (const partyId of joinOrder) {
769
+ (await post(keygenPath, { message: round0Message(partyId), version: 1.2 }, partyAuth(partyId))).status.should.equal(200);
770
+ }
771
+ };
772
+
773
+ it('should authenticate every party when round 0 messages arrive in partyId order', async function() {
774
+ await runRound0([1, 2]);
775
+ for (const partyId of [0, 1, 2]) {
776
+ await expectRound0Messages(partyAuth(partyId));
777
+ }
778
+ });
779
+
780
+ it('should authenticate every party when round 0 messages arrive out of partyId order', async function() {
781
+ await runRound0([2, 1]);
782
+ for (const partyId of [0, 1, 2]) {
783
+ await expectRound0Messages(partyAuth(partyId));
784
+ }
785
+ });
786
+
787
+ it('should reject another party identity', async function() {
788
+ await createKeyGenSession({ joined: [1, 2] });
789
+ for (const [partyId, otherPartyId] of [[1, 2], [2, 1]]) {
790
+ const res = await get(path, { copayerId: copayerIds[otherPartyId], privKey: parties[partyId].authKey });
791
+ expectError(res, 401, 'NOT_AUTHORIZED', 'Invalid signature');
792
+ }
793
+ });
794
+
795
+ it('should error if a registered party has not joined yet', async function() {
796
+ await createKeyGenSession({ registered: [2], joined: [1] });
797
+ const res = await get(path, partyAuth(2));
798
+ expectError(res, 401, 'NOT_AUTHORIZED', 'Not authorized');
799
+ });
800
+
801
+ it('should error if the copayer is not in the session', async function() {
802
+ await createKeyGenSession({ joined: [1, 2] });
803
+ const res = await get(path, { copayerId: 'notInSession', privKey: randomKey() });
804
+ expectError(res, 401, 'NOT_AUTHORIZED', 'Not authorized');
805
+ });
806
+
807
+ it('should error if the session does not exist', async function() {
808
+ const res = await get(path, partyAuth(0));
809
+ expectError(res, 401, 'NOT_AUTHORIZED', 'Session not found');
810
+ });
811
+
812
+ it('should error if session is expired', async function() {
813
+ await createKeyGenSession({ joined: [1, 2], timeLimit: 1 });
814
+ expireSession();
815
+ const res = await get(path, partyAuth(0));
816
+ expectError(res, 400, 'TSS_SESSION_EXPIRED');
817
+ });
818
+
819
+ describe('legacy (v1.1) session', function() {
820
+ it('should authenticate every party with the publicKey from its round 0 message', async function() {
821
+ await createKeyGenSession({ version: 1.1, joined: [1, 2] });
822
+ for (const partyId of [0, 1, 2]) {
823
+ await expectRound0Messages(partyAuth(partyId));
824
+ }
825
+ });
826
+
827
+ it('should reject another party identity', async function() {
828
+ await createKeyGenSession({ version: 1.1, joined: [1, 2] });
829
+ for (const [partyId, otherPartyId] of [[1, 2], [2, 1]]) {
830
+ const res = await get(path, { copayerId: copayerIds[otherPartyId], privKey: parties[partyId].authKey });
831
+ expectError(res, 401, 'NOT_AUTHORIZED', 'Invalid signature');
832
+ }
833
+ });
834
+
835
+ it('should error if the copayer is not in the session', async function() {
836
+ await createKeyGenSession({ version: 1.1, joined: [1, 2] });
837
+ const res = await get(path, { copayerId: 'notInSession', privKey: randomKey() });
838
+ expectError(res, 401, 'NOT_AUTHORIZED', 'Copayer not found in session');
839
+ });
840
+ });
446
841
  });
447
842
  });
448
843
 
449
- describe('POST /v1/tss/sign/:id', function() {
450
- const url = id => `${urlPrefix}/v1/tss/sign/${id}`;
451
- const signRequest = body => {
452
- const message = `post|${url('test').replace(urlPrefix, '')}|${JSON.stringify(body)}`;
453
- const hash = Utils.hashMessage(message, false); // BWC hashes the message with reversal, then signs
454
- const xSignature = BitcoreLib.crypto.ECDSA.sign(hash, privateKey, { endian: 'little' }).toString();
455
- return xSignature;
844
+ describe('Signing', function() {
845
+ // party 0 & 1 are signing; party 2 is a wallet copayer who is not part of the signing session
846
+ const sigMessage = (partyId: number) => ({
847
+ round: 0,
848
+ partyId,
849
+ publicKey: authPubKey(partyId),
850
+ broadcastMessages: [],
851
+ p2pMessages: []
852
+ });
853
+
854
+ const createSigSession = async (opts: { timeLimit?: number } = {}) => {
855
+ const doc = TssSigGenModel.create({
856
+ id: SESSION_ID,
857
+ message: sigMessage(0) as any,
858
+ m: 2,
859
+ copayerId: copayerIds[0],
860
+ version: 1.1,
861
+ timeLimit: opts.timeLimit
862
+ });
863
+ doc.participants.push({ partyId: 1, copayerId: copayerIds[1] });
864
+ doc.rounds[0].push({ fromPartyId: 1, messages: sigMessage(1) as any });
865
+ await storage.storeTssSigSession({ doc });
456
866
  };
457
- const privateKey = new BitcoreLib.PrivateKey('cd643c4f2e68b669d97c51877905e8d77efc98cabb5a36a8eda66bc143055d37');
458
- const publicKey = privateKey.publicKey.toString();
459
867
 
460
868
  beforeEach(async function() {
461
- const storage = helpers.getStorage();
462
869
  await storage.db.collection(BWS.Storage.collections.TSS_SIGN).deleteMany({});
463
870
  await storage.db.collection(BWS.Storage.collections.COPAYERS_LOOKUP).deleteMany({});
464
- await storage.db.collection(BWS.Storage.collections.COPAYERS_LOOKUP).insertOne({
465
- copayerId: publicKey,
871
+ await storage.db.collection(BWS.Storage.collections.COPAYERS_LOOKUP).insertMany([0, 1, 2].map(partyId => ({
872
+ copayerId: copayerIds[partyId],
466
873
  walletId: 'test',
467
- requestPubKey: publicKey,
468
- });
874
+ requestPubKey: authPubKey(partyId),
875
+ })));
469
876
  });
470
877
 
471
- it('should error if session is expired', function(done) {
472
- const storage = helpers.getStorage();
473
- const sigGen = TssSigGenModel.create({
474
- id: 'test',
475
- message: { partyId: 0, publicKey } as any,
476
- m: 2,
477
- copayerId: publicKey,
478
- version: 1.1,
479
- timeLimit: 1, // 1 minute
480
- });
481
- storage.storeTssSigSession({ doc: sigGen }).then(() => {
482
- // advance time by 2 minutes
483
- const now = Date.now() + 2 * 60 * 1000;
484
- sandbox.stub(Date, 'now').returns(now);
485
-
486
- const message = { publicKey, p2pMessages: [{ payload: { encryptedMessage: 'encryptie', signature: 'siggie' } }], broadcastMessages: [] };
487
- const xSignature = signRequest({ message });
488
-
489
- app.post(url('test'))
490
- .set('x-identity', publicKey)
491
- .set('x-signature', xSignature)
492
- .send({ message })
493
- .expect((res) => {
494
- res.status.should.equal(400);
495
- res.body.code.should.equal('TSS_SESSION_EXPIRED');
496
- })
497
- .end(done);
498
- }).catch(done);
878
+ describe('POST /v1/tss/sign/:id', function() {
879
+ const path = `/v1/tss/sign/${SESSION_ID}`;
880
+
881
+ it('should error if session is expired', async function() {
882
+ await createSigSession({ timeLimit: 1 });
883
+ expireSession();
884
+
885
+ const message = { ...sigMessage(1), round: 1, p2pMessages: [{ payload: { encryptedMessage: 'encryptie', signature: 'siggie' } }] };
886
+ const res = await post(path, { message }, partyAuth(1));
887
+ expectError(res, 400, 'TSS_SESSION_EXPIRED');
888
+ });
499
889
  });
500
- });
501
890
 
502
- describe('POST /v1/tss/sign/:id/store', function() {
503
- const url = id => `${urlPrefix}/v1/tss/sign/${id}/store`;
504
- const signRequest = body => {
505
- const message = `post|${url('test').replace(urlPrefix, '')}|${JSON.stringify(body)}`;
506
- const hash = Utils.hashMessage(message, false); // BWC hashes the message with reversal, then signs
507
- const xSignature = BitcoreLib.crypto.ECDSA.sign(hash, privateKey, { endian: 'little' }).toString();
508
- return xSignature;
509
- };
510
- const privateKey = new BitcoreLib.PrivateKey('cd643c4f2e68b669d97c51877905e8d77efc98cabb5a36a8eda66bc143055d37');
511
- const publicKey = privateKey.publicKey.toString();
512
- const signature = {
513
- r: '0x71a8fb6b23203f7ae7f5df7dac410204d8b8d8b74e31c3919e0c764a00925f74',
514
- s: '0x18048c9ce4a860f6354b4bb795178ff3cde858ed3afef6c2e2c51c8fac8b9257',
515
- v: 1,
516
- pubKey: '0267c2c572fcc1a5026d0e5d8e60a04ff6adeaf1a3a996c9c69f3b1714cff1db85',
517
- };
891
+ describe('GET /v1/tss/sign/:id/:round', function() {
892
+ const path = `/v1/tss/sign/${SESSION_ID}/0?maxWaitTime=0`;
518
893
 
894
+ it('should return round messages to a session participant', async function() {
895
+ await createSigSession();
896
+ const res = await get(path, partyAuth(0));
897
+ res.status.should.equal(200);
898
+ const body = JSON.parse(res.text);
899
+ body.messages.length.should.equal(1);
900
+ body.participants.should.deep.equal([copayerIds[1]]);
901
+ });
519
902
 
520
- beforeEach(async function() {
521
- const storage = helpers.getStorage();
522
- await storage.db.collection(BWS.Storage.collections.TSS_SIGN).deleteMany({});
523
- const sigGen = TssSigGenModel.create({
524
- id: 'test',
525
- message: { partyId: 0, publicKey } as any,
526
- m: 2,
527
- copayerId: publicKey,
528
- version: 1.1,
903
+ it('should error if the copayer is not registered', async function() {
904
+ await createSigSession();
905
+ const res = await get(path, { copayerId: 'unknownCopayer', privKey: randomKey() });
906
+ expectError(res, 401, 'NOT_AUTHORIZED', 'Copayer not found');
529
907
  });
530
- await storage.storeTssSigSession({ doc: sigGen });
531
- await storage.storeTssSignature({ id: 'test', signature });
532
- });
533
908
 
534
- it('should throw a mismatch error for a mismatched signature: r', function(done) {
535
- const modifiedSignature = {
536
- ...signature,
537
- r: '0x71a8fb6b23203f7ae7f5df7dac410204d8b8d8b74e31c3919e0c764a00925f75', // modified r value to create a mismatch
538
- };
539
- const xSignature = signRequest({ signature: modifiedSignature });
540
-
541
- app.post(url('test'))
542
- .set('x-identity', publicKey)
543
- .set('x-signature', xSignature)
544
- .send({
545
- signature: modifiedSignature
546
- })
547
- .expect(res => {
548
- res.status.should.equal(400);
549
- res.body.should.have.property('code');
550
- res.body.code.should.equal('TSS_FINAL_SIGNATURE_MISMATCH');
551
- res.body.message.should.equal('Final signature does not match the one generated by the session');
552
- })
553
- .end(done);
554
- });
909
+ it('should error if the copayer is not a session participant', async function() {
910
+ await createSigSession();
911
+ const res = await get(path, partyAuth(2));
912
+ expectError(res, 401, 'NOT_AUTHORIZED', 'Copayer not found in session');
913
+ });
555
914
 
556
- it('should throw a mismatch error for a mismatched signature: s', function(done) {
557
- const modifiedSignature = {
558
- ...signature,
559
- s: '0x18048c9ce4a860f6354b4bb795178ff3cde858ed3afef6c2e2c51c8fac8b9256', // modified s value to create a mismatch
560
- };
561
- const xSignature = signRequest({ signature: modifiedSignature });
562
-
563
- app.post(url('test'))
564
- .set('x-identity', publicKey)
565
- .set('x-signature', xSignature)
566
- .send({
567
- signature: modifiedSignature
568
- })
569
- .expect(res => {
570
- res.status.should.equal(400);
571
- res.body.should.have.property('code');
572
- res.body.code.should.equal('TSS_FINAL_SIGNATURE_MISMATCH');
573
- res.body.message.should.equal('Final signature does not match the one generated by the session');
574
- })
575
- .end(done);
576
- });
915
+ it('should error if the request signature is invalid', async function() {
916
+ await createSigSession();
917
+ const res = await get(path, { copayerId: copayerIds[0], privKey: randomKey() });
918
+ expectError(res, 401, 'NOT_AUTHORIZED', 'Invalid signature');
919
+ });
577
920
 
578
- it('should throw a mismatch error for a mismatched signature: v', function(done) {
579
- const modifiedSignature = {
580
- ...signature,
581
- v: 123, // modified v value to create a mismatch
582
- };
583
- const xSignature = signRequest({ signature: modifiedSignature });
584
-
585
- app.post(url('test'))
586
- .set('x-identity', publicKey)
587
- .set('x-signature', xSignature)
588
- .send({
589
- signature: modifiedSignature
590
- })
591
- .expect(res => {
592
- res.status.should.equal(400);
593
- res.body.should.have.property('code');
594
- res.body.code.should.equal('TSS_FINAL_SIGNATURE_MISMATCH');
595
- res.body.message.should.equal('Final signature does not match the one generated by the session');
596
- })
597
- .end(done);
921
+ it('should reject another participant identity', async function() {
922
+ await createSigSession();
923
+ const res = await get(path, { copayerId: copayerIds[1], privKey: parties[0].authKey });
924
+ expectError(res, 401, 'NOT_AUTHORIZED', 'Invalid signature');
925
+ });
926
+
927
+ it('should error if the session does not exist', async function() {
928
+ const res = await get(path, partyAuth(0));
929
+ expectError(res, 400, 'TSS_SESSION_NOT_FOUND');
930
+ });
931
+
932
+ it('should error if session is expired', async function() {
933
+ await createSigSession({ timeLimit: 1 });
934
+ expireSession();
935
+ const res = await get(path, partyAuth(0));
936
+ expectError(res, 400, 'TSS_SESSION_EXPIRED');
937
+ });
598
938
  });
599
939
 
600
- it('should throw a mismatch error for a mismatched signature: pubKey', function(done) {
601
- const modifiedSignature = {
602
- ...signature,
603
- pubKey: 'not-equal-pub-key', // modified pubKey value to create a mismatch
940
+ describe('POST /v1/tss/sign/:id/store', function() {
941
+ const path = `/v1/tss/sign/${SESSION_ID}/store`;
942
+ const signature = {
943
+ r: '0x71a8fb6b23203f7ae7f5df7dac410204d8b8d8b74e31c3919e0c764a00925f74',
944
+ s: '0x18048c9ce4a860f6354b4bb795178ff3cde858ed3afef6c2e2c51c8fac8b9257',
945
+ v: 1,
946
+ pubKey: '0267c2c572fcc1a5026d0e5d8e60a04ff6adeaf1a3a996c9c69f3b1714cff1db85',
947
+ };
948
+ const expectMismatch = res => expectError(res, 400, 'TSS_FINAL_SIGNATURE_MISMATCH', 'Final signature does not match the one generated by the session');
949
+
950
+ /** Make the session look unsigned for the first 2 fetches (middleware + storeSignature) to simulate a concurrent store */
951
+ const stubStoreRaceCondition = () => {
952
+ const fetchSessionStub = sandbox.stub(BWS.Storage.prototype, 'fetchTssSigSession');
953
+ fetchSessionStub.callsFake(async function({ id }) {
954
+ const session = await fetchSessionStub.wrappedMethod.call(this, { id });
955
+ session.signature = null;
956
+ return session;
957
+ });
958
+ fetchSessionStub.onThirdCall().callsFake(async function({ id }) {
959
+ return fetchSessionStub.wrappedMethod.call(this, { id });
960
+ });
604
961
  };
605
- const xSignature = signRequest({ signature: modifiedSignature });
606
-
607
- app.post(url('test'))
608
- .set('x-identity', publicKey)
609
- .set('x-signature', xSignature)
610
- .send({
611
- signature: modifiedSignature
612
- })
613
- .expect(res => {
614
- res.status.should.equal(400);
615
- res.body.should.have.property('code');
616
- res.body.code.should.equal('TSS_FINAL_SIGNATURE_MISMATCH');
617
- res.body.message.should.equal('Final signature does not match the one generated by the session');
618
- })
619
- .end(done);
620
- });
621
962
 
622
- it('should cleanly handle race condition when saving signature', function(done) {
623
- const fetchSessionStub = sandbox.stub(BWS.Storage.prototype, 'fetchTssSigSession');
624
- fetchSessionStub.callsFake(async function({ id }) {
625
- const session = await fetchSessionStub.wrappedMethod.call(this, { id });
626
- session.signature = null;
627
- return session;
628
- });
629
- fetchSessionStub.onThirdCall().callsFake(async function({ id }) {
630
- const session = await fetchSessionStub.wrappedMethod.call(this, { id });
631
- return session;
632
- });
633
- const xSignature = signRequest({ signature });
634
-
635
- app.post(url('test'))
636
- .set('x-identity', publicKey)
637
- .set('x-signature', xSignature)
638
- .send({
639
- signature
640
- })
641
- .expect(res => {
642
- res.status.should.equal(200);
643
- })
644
- .end(done);
645
- });
963
+ beforeEach(async function() {
964
+ await createSigSession();
965
+ await storage.storeTssSignature({ id: SESSION_ID, signature });
966
+ });
646
967
 
647
- it('should handle race condition when saving signature but mismatched signature', function(done) {
648
- const fetchSessionStub = sandbox.stub(BWS.Storage.prototype, 'fetchTssSigSession');
649
- fetchSessionStub.callsFake(async function({ id }) {
650
- const session = await fetchSessionStub.wrappedMethod.call(this, { id });
651
- session.signature = null;
652
- return session;
968
+ it('should accept a signature matching the session signature', async function() {
969
+ const res = await post(path, { signature }, partyAuth(1));
970
+ res.status.should.equal(200);
653
971
  });
654
- fetchSessionStub.onThirdCall().callsFake(async function({ id }) {
655
- const session = await fetchSessionStub.wrappedMethod.call(this, { id });
656
- return session;
972
+
973
+ for (const [field, value] of [
974
+ ['r', '0x71a8fb6b23203f7ae7f5df7dac410204d8b8d8b74e31c3919e0c764a00925f75'],
975
+ ['s', '0x18048c9ce4a860f6354b4bb795178ff3cde858ed3afef6c2e2c51c8fac8b9256'],
976
+ ['v', 123],
977
+ ['pubKey', 'not-equal-pub-key'],
978
+ ] as Array<[string, string | number]>) {
979
+ it(`should throw a mismatch error for a mismatched signature: ${field}`, async function() {
980
+ const res = await post(path, { signature: { ...signature, [field]: value } }, partyAuth(0));
981
+ expectMismatch(res);
982
+ });
983
+ }
984
+
985
+ it('should cleanly handle race condition when saving signature', async function() {
986
+ stubStoreRaceCondition();
987
+ const res = await post(path, { signature }, partyAuth(0));
988
+ res.status.should.equal(200);
657
989
  });
658
990
 
659
- const modifiedSignature = {
660
- ...signature,
661
- r: '0x71a8fb6b23203f7ae7f5df7dac410204d8b8d8b74e31c3919e0c764a00925f75', // modified r value to create a mismatch
662
- };
991
+ it('should handle race condition when saving signature but mismatched signature', async function() {
992
+ stubStoreRaceCondition();
993
+ const modifiedSignature = {
994
+ ...signature,
995
+ r: '0x71a8fb6b23203f7ae7f5df7dac410204d8b8d8b74e31c3919e0c764a00925f75', // modified r value to create a mismatch
996
+ };
997
+ const res = await post(path, { signature: modifiedSignature }, partyAuth(0));
998
+ expectMismatch(res);
999
+ });
1000
+
1001
+ it('should error if the copayer is not registered', async function() {
1002
+ const res = await post(path, { signature }, { copayerId: 'unknownCopayer', privKey: randomKey() });
1003
+ expectError(res, 401, 'NOT_AUTHORIZED', 'Copayer not found');
1004
+ });
1005
+
1006
+ it('should error if the copayer is not a session participant', async function() {
1007
+ const res = await post(path, { signature }, partyAuth(2));
1008
+ expectError(res, 401, 'NOT_AUTHORIZED', 'Copayer not found in session');
1009
+ });
663
1010
 
664
- const xSignature = signRequest({ signature: modifiedSignature });
665
-
666
- app.post(url('test'))
667
- .set('x-identity', publicKey)
668
- .set('x-signature', xSignature)
669
- .send({
670
- signature: modifiedSignature
671
- })
672
- .expect(res => {
673
- res.status.should.equal(400);
674
- res.body.should.have.property('code');
675
- res.body.code.should.equal('TSS_FINAL_SIGNATURE_MISMATCH');
676
- res.body.message.should.equal('Final signature does not match the one generated by the session');
677
- })
678
- .end(done);
1011
+ it('should error if the request signature is invalid', async function() {
1012
+ const res = await post(path, { signature }, { copayerId: copayerIds[0], privKey: randomKey() });
1013
+ expectError(res, 401, 'NOT_AUTHORIZED', 'Invalid signature');
1014
+ });
679
1015
  });
680
1016
  });
681
-
682
- });
1017
+ });