@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.
- package/package.json +5 -5
- package/src/config.ts +0 -20
- package/src/externalservices/banxa.ts +51 -10
- package/src/externalservices/changelly.ts +54 -6
- package/src/externalservices/moonpay.ts +85 -19
- package/src/externalservices/oneInch.ts +13 -5
- package/src/externalservices/ramp.ts +42 -8
- package/src/externalservices/sardine.ts +51 -10
- package/src/externalservices/simplex.ts +58 -11
- package/src/externalservices/thorswap.ts +46 -11
- package/src/externalservices/transak.ts +53 -11
- 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/services.ts +0 -12
- package/src/lib/routes/tss.ts +21 -8
- package/src/lib/routes/wallets.ts +8 -9
- package/src/lib/server.ts +9 -20
- package/src/lib/storage.ts +55 -22
- package/src/lib/tss.ts +77 -1
- package/src/types/externalservices.d.ts +0 -1
- package/test/integration/externalservices/banxa.test.ts +95 -5
- package/test/integration/externalservices/changelly.test.ts +118 -6
- package/test/integration/externalservices/coinGecko.test.ts +4 -0
- package/test/integration/externalservices/moonpay.test.ts +178 -9
- package/test/integration/externalservices/oneInch.test.ts +28 -1
- package/test/integration/externalservices/ramp.test.ts +126 -4
- package/test/integration/externalservices/sardine.test.ts +95 -5
- package/test/integration/externalservices/simplex.test.ts +95 -5
- package/test/integration/externalservices/thorswap.test.ts +81 -5
- package/test/integration/externalservices/transak.test.ts +98 -6
- package/test/integration/helpers.ts +1 -1
- package/test/integration/server.test.ts +56 -88
- 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/config.d.ts.map +1 -1
- package/ts_build/src/config.js +0 -4
- package/ts_build/src/config.js.map +1 -1
- package/ts_build/src/externalservices/banxa.d.ts.map +1 -1
- package/ts_build/src/externalservices/banxa.js +51 -10
- package/ts_build/src/externalservices/banxa.js.map +1 -1
- package/ts_build/src/externalservices/changelly.d.ts.map +1 -1
- package/ts_build/src/externalservices/changelly.js +54 -6
- package/ts_build/src/externalservices/changelly.js.map +1 -1
- package/ts_build/src/externalservices/moonpay.d.ts.map +1 -1
- package/ts_build/src/externalservices/moonpay.js +85 -19
- package/ts_build/src/externalservices/moonpay.js.map +1 -1
- package/ts_build/src/externalservices/oneInch.d.ts.map +1 -1
- package/ts_build/src/externalservices/oneInch.js +13 -5
- package/ts_build/src/externalservices/oneInch.js.map +1 -1
- package/ts_build/src/externalservices/ramp.d.ts.map +1 -1
- package/ts_build/src/externalservices/ramp.js +42 -8
- package/ts_build/src/externalservices/ramp.js.map +1 -1
- package/ts_build/src/externalservices/sardine.d.ts.map +1 -1
- package/ts_build/src/externalservices/sardine.js +51 -10
- package/ts_build/src/externalservices/sardine.js.map +1 -1
- package/ts_build/src/externalservices/simplex.d.ts.map +1 -1
- package/ts_build/src/externalservices/simplex.js +55 -11
- package/ts_build/src/externalservices/simplex.js.map +1 -1
- package/ts_build/src/externalservices/thorswap.d.ts.map +1 -1
- package/ts_build/src/externalservices/thorswap.js +45 -11
- package/ts_build/src/externalservices/thorswap.js.map +1 -1
- package/ts_build/src/externalservices/transak.d.ts.map +1 -1
- package/ts_build/src/externalservices/transak.js +53 -11
- package/ts_build/src/externalservices/transak.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/index.d.ts +1 -1
- package/ts_build/src/lib/errors/errordefinitions.d.ts +4 -0
- package/ts_build/src/lib/errors/errordefinitions.d.ts.map +1 -1
- package/ts_build/src/lib/errors/errordefinitions.js +4 -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/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/services.d.ts.map +1 -1
- package/ts_build/src/lib/routes/services.js +0 -10
- package/ts_build/src/lib/routes/services.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 +8 -9
- package/ts_build/src/lib/routes/wallets.js.map +1 -1
- package/ts_build/src/lib/server.d.ts +0 -2
- package/ts_build/src/lib/server.d.ts.map +1 -1
- package/ts_build/src/lib/server.js +8 -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/integration/externalservices/banxa.test.d.ts.map +1 -1
- package/ts_build/test/integration/externalservices/banxa.test.js +93 -5
- package/ts_build/test/integration/externalservices/banxa.test.js.map +1 -1
- package/ts_build/test/integration/externalservices/changelly.test.d.ts.map +1 -1
- package/ts_build/test/integration/externalservices/changelly.test.js +111 -6
- package/ts_build/test/integration/externalservices/changelly.test.js.map +1 -1
- package/ts_build/test/integration/externalservices/coinGecko.test.js +4 -0
- package/ts_build/test/integration/externalservices/coinGecko.test.js.map +1 -1
- package/ts_build/test/integration/externalservices/moonpay.test.d.ts.map +1 -1
- package/ts_build/test/integration/externalservices/moonpay.test.js +169 -9
- package/ts_build/test/integration/externalservices/moonpay.test.js.map +1 -1
- package/ts_build/test/integration/externalservices/oneInch.test.d.ts.map +1 -1
- package/ts_build/test/integration/externalservices/oneInch.test.js +26 -1
- package/ts_build/test/integration/externalservices/oneInch.test.js.map +1 -1
- package/ts_build/test/integration/externalservices/ramp.test.d.ts.map +1 -1
- package/ts_build/test/integration/externalservices/ramp.test.js +115 -4
- package/ts_build/test/integration/externalservices/ramp.test.js.map +1 -1
- package/ts_build/test/integration/externalservices/sardine.test.d.ts.map +1 -1
- package/ts_build/test/integration/externalservices/sardine.test.js +94 -5
- package/ts_build/test/integration/externalservices/sardine.test.js.map +1 -1
- package/ts_build/test/integration/externalservices/simplex.test.d.ts.map +1 -1
- package/ts_build/test/integration/externalservices/simplex.test.js +89 -5
- package/ts_build/test/integration/externalservices/simplex.test.js.map +1 -1
- package/ts_build/test/integration/externalservices/thorswap.test.d.ts.map +1 -1
- package/ts_build/test/integration/externalservices/thorswap.test.js +76 -5
- package/ts_build/test/integration/externalservices/thorswap.test.js.map +1 -1
- package/ts_build/test/integration/externalservices/transak.test.d.ts.map +1 -1
- package/ts_build/test/integration/externalservices/transak.test.js +93 -6
- package/ts_build/test/integration/externalservices/transak.test.js.map +1 -1
- package/ts_build/test/integration/helpers.js +1 -1
- package/ts_build/test/integration/helpers.js.map +1 -1
- package/ts_build/test/integration/server.test.js +44 -76
- 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/storage.test.js +6 -7
- package/ts_build/test/storage.test.js.map +1 -1
- package/src/externalservices/wyre.ts +0 -131
- package/test/integration/externalservices/wyre.test.ts +0 -270
- package/ts_build/src/externalservices/wyre.d.ts +0 -7
- package/ts_build/src/externalservices/wyre.d.ts.map +0 -1
- package/ts_build/src/externalservices/wyre.js +0 -144
- package/ts_build/src/externalservices/wyre.js.map +0 -1
- package/ts_build/test/integration/externalservices/wyre.test.d.ts +0 -2
- package/ts_build/test/integration/externalservices/wyre.test.d.ts.map +0 -1
- package/ts_build/test/integration/externalservices/wyre.test.js +0 -281
- 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
|
-
|
|
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('
|
|
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('
|
|
265
|
-
|
|
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
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
.
|
|
275
|
-
.
|
|
276
|
-
|
|
277
|
-
|
|
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
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
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
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
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
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
.
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
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
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
.
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
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
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
};
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
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
|
-
|
|
352
|
-
const
|
|
353
|
-
const
|
|
354
|
-
|
|
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
|
-
|
|
383
|
-
|
|
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
|
-
|
|
397
|
-
|
|
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
|
-
|
|
406
|
-
|
|
407
|
-
|
|
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
|
-
|
|
411
|
-
|
|
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
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
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
|
-
|
|
423
|
-
|
|
424
|
-
await joinRound0(2);
|
|
425
|
-
await joinRound0(1);
|
|
697
|
+
it('should accept a numeric string partyId', async function() {
|
|
698
|
+
await createKeyGenSession();
|
|
426
699
|
|
|
427
|
-
|
|
428
|
-
const res = await getRound0(partyId);
|
|
700
|
+
const res = await register('1', authPubKey(1));
|
|
429
701
|
res.status.should.equal(200);
|
|
430
|
-
|
|
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
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
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('
|
|
450
|
-
|
|
451
|
-
const
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
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).
|
|
465
|
-
copayerId:
|
|
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:
|
|
468
|
-
});
|
|
874
|
+
requestPubKey: authPubKey(partyId),
|
|
875
|
+
})));
|
|
469
876
|
});
|
|
470
877
|
|
|
471
|
-
|
|
472
|
-
const
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
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
|
-
|
|
503
|
-
|
|
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
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
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
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
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
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
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
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
};
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
})
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
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
|
-
|
|
601
|
-
const
|
|
602
|
-
|
|
603
|
-
|
|
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
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
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
|
-
|
|
648
|
-
|
|
649
|
-
|
|
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
|
-
|
|
655
|
-
|
|
656
|
-
|
|
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
|
-
|
|
660
|
-
|
|
661
|
-
|
|
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
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
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
|
+
});
|