@bitpay-labs/bitcore-wallet-service 11.10.13 → 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.
- package/package.json +5 -5
- package/src/externalservices/moonpay.ts +6 -2
- package/src/lib/common/constants.ts +1 -1
- package/src/lib/errors/errordefinitions.ts +8 -0
- package/src/lib/model/tsskeygen.ts +12 -4
- package/src/lib/routes/middleware/authRequest.ts +6 -16
- package/src/lib/routes/middleware/authTssRequest.ts +130 -26
- package/src/lib/routes/tss.ts +21 -8
- package/src/lib/routes/wallets.ts +8 -9
- package/src/lib/server.ts +9 -17
- package/src/lib/storage.ts +55 -22
- package/src/lib/tss.ts +77 -1
- package/test/integration/externalservices/moonpay.test.ts +50 -0
- package/test/integration/tss.test.ts +706 -371
- package/test/storage.test.ts +6 -7
- package/ts_build/package.json +4 -4
- package/ts_build/src/externalservices/moonpay.d.ts.map +1 -1
- package/ts_build/src/externalservices/moonpay.js +8 -3
- package/ts_build/src/externalservices/moonpay.js.map +1 -1
- package/ts_build/src/lib/chain/btc/index.js +7 -7
- package/ts_build/src/lib/chain/btc/index.js.map +1 -1
- package/ts_build/src/lib/chain/eth/index.js +1 -1
- package/ts_build/src/lib/chain/eth/index.js.map +1 -1
- package/ts_build/src/lib/chain/sol/index.d.ts +2 -0
- package/ts_build/src/lib/chain/sol/index.d.ts.map +1 -1
- package/ts_build/src/lib/chain/sol/index.js +36 -3
- package/ts_build/src/lib/chain/sol/index.js.map +1 -1
- package/ts_build/src/lib/chain/xrp/index.js +2 -2
- package/ts_build/src/lib/chain/xrp/index.js.map +1 -1
- package/ts_build/src/lib/common/constants.d.ts +1 -1
- package/ts_build/src/lib/common/constants.js +1 -1
- package/ts_build/src/lib/common/defaults.d.ts +3 -0
- package/ts_build/src/lib/common/defaults.d.ts.map +1 -1
- package/ts_build/src/lib/common/defaults.js +3 -0
- package/ts_build/src/lib/common/defaults.js.map +1 -1
- package/ts_build/src/lib/common/index.d.ts +4 -1
- package/ts_build/src/lib/common/index.d.ts.map +1 -1
- package/ts_build/src/lib/errors/errordefinitions.d.ts +6 -0
- package/ts_build/src/lib/errors/errordefinitions.d.ts.map +1 -1
- package/ts_build/src/lib/errors/errordefinitions.js +6 -0
- package/ts_build/src/lib/errors/errordefinitions.js.map +1 -1
- package/ts_build/src/lib/model/tsskeygen.d.ts +2 -0
- package/ts_build/src/lib/model/tsskeygen.d.ts.map +1 -1
- package/ts_build/src/lib/model/tsskeygen.js +6 -2
- package/ts_build/src/lib/model/tsskeygen.js.map +1 -1
- package/ts_build/src/lib/model/txproposal.d.ts +3 -2
- package/ts_build/src/lib/model/txproposal.d.ts.map +1 -1
- package/ts_build/src/lib/model/txproposal.js +33 -9
- package/ts_build/src/lib/model/txproposal.js.map +1 -1
- package/ts_build/src/lib/routes/context.d.ts.map +1 -1
- package/ts_build/src/lib/routes/context.js +3 -1
- package/ts_build/src/lib/routes/context.js.map +1 -1
- package/ts_build/src/lib/routes/helpers/error.js +2 -2
- package/ts_build/src/lib/routes/helpers/error.js.map +1 -1
- package/ts_build/src/lib/routes/middleware/authRequest.d.ts.map +1 -1
- package/ts_build/src/lib/routes/middleware/authRequest.js +6 -16
- package/ts_build/src/lib/routes/middleware/authRequest.js.map +1 -1
- package/ts_build/src/lib/routes/middleware/authTssRequest.d.ts +2 -1
- package/ts_build/src/lib/routes/middleware/authTssRequest.d.ts.map +1 -1
- package/ts_build/src/lib/routes/middleware/authTssRequest.js +104 -24
- package/ts_build/src/lib/routes/middleware/authTssRequest.js.map +1 -1
- package/ts_build/src/lib/routes/transactions.js +10 -5
- package/ts_build/src/lib/routes/transactions.js.map +1 -1
- package/ts_build/src/lib/routes/tss.d.ts.map +1 -1
- package/ts_build/src/lib/routes/tss.js +19 -7
- package/ts_build/src/lib/routes/tss.js.map +1 -1
- package/ts_build/src/lib/routes/wallets.d.ts.map +1 -1
- package/ts_build/src/lib/routes/wallets.js +12 -11
- package/ts_build/src/lib/routes/wallets.js.map +1 -1
- package/ts_build/src/lib/server.d.ts.map +1 -1
- package/ts_build/src/lib/server.js +14 -20
- package/ts_build/src/lib/server.js.map +1 -1
- package/ts_build/src/lib/storage.d.ts +9 -2
- package/ts_build/src/lib/storage.d.ts.map +1 -1
- package/ts_build/src/lib/storage.js +51 -20
- package/ts_build/src/lib/storage.js.map +1 -1
- package/ts_build/src/lib/tss.d.ts +8 -0
- package/ts_build/src/lib/tss.d.ts.map +1 -1
- package/ts_build/src/lib/tss.js +59 -1
- package/ts_build/src/lib/tss.js.map +1 -1
- package/ts_build/test/chain/btc.test.js +26 -0
- package/ts_build/test/chain/btc.test.js.map +1 -1
- package/ts_build/test/chain/eth.test.js +40 -0
- package/ts_build/test/chain/eth.test.js.map +1 -1
- package/ts_build/test/chain/sol.test.js +156 -0
- package/ts_build/test/chain/sol.test.js.map +1 -1
- package/ts_build/test/chain/xrp.test.js +18 -0
- package/ts_build/test/chain/xrp.test.js.map +1 -1
- package/ts_build/test/integration/externalservices/moonpay.test.js +67 -0
- package/ts_build/test/integration/externalservices/moonpay.test.js.map +1 -1
- package/ts_build/test/integration/server.test.js +87 -0
- package/ts_build/test/integration/server.test.js.map +1 -1
- package/ts_build/test/integration/tss.test.d.ts.map +1 -1
- package/ts_build/test/integration/tss.test.js +599 -357
- package/ts_build/test/integration/tss.test.js.map +1 -1
- package/ts_build/test/model/txproposal.test.js +101 -0
- package/ts_build/test/model/txproposal.test.js.map +1 -1
- package/ts_build/test/numberformat-routes.test.d.ts +2 -0
- package/ts_build/test/numberformat-routes.test.d.ts.map +1 -0
- package/ts_build/test/numberformat-routes.test.js +116 -0
- package/ts_build/test/numberformat-routes.test.js.map +1 -0
- package/ts_build/test/storage.test.js +6 -7
- 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
|
-
|
|
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('
|
|
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('
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
.
|
|
296
|
-
.
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
.
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
res.status.should.equal(
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
res
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
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
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
should.
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
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
|
-
|
|
368
|
-
const
|
|
369
|
-
const
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
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
|
-
|
|
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
|
-
|
|
429
|
-
|
|
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
|
-
|
|
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
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
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('
|
|
452
|
-
const
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
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
|
-
|
|
472
|
-
const
|
|
473
|
-
|
|
474
|
-
|
|
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:
|
|
768
|
+
copayerId: copayerIds[0],
|
|
478
769
|
version: 1.1,
|
|
479
|
-
timeLimit:
|
|
480
|
-
});
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
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
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
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
|
-
|
|
528
|
-
const
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
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
|
-
|
|
548
|
-
const
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
.
|
|
555
|
-
.
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
res
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
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
|
-
|
|
568
|
-
const
|
|
569
|
-
|
|
570
|
-
|
|
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
|
|
573
|
-
|
|
574
|
-
.
|
|
575
|
-
.
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
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
|
-
|
|
593
|
-
|
|
594
|
-
.
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
signature
|
|
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
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
signature: modifiedSignature
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
res
|
|
655
|
-
res
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
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
|
});
|