@bitpay-labs/bitcore-wallet-service 11.11.2 → 11.11.3
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 +2 -2
- package/src/lib/common/constants.ts +1 -1
- package/src/lib/common/utils.ts +18 -1
- package/src/lib/model/tsskeygen.ts +16 -0
- package/src/lib/routes/maintenance.ts +11 -2
- package/src/lib/routes/middleware/authTssRequest.ts +28 -16
- package/src/lib/routes/middleware/verifyTssMessage.ts +4 -1
- package/src/lib/routes/services.ts +7 -1
- package/src/lib/routes/tss.ts +33 -30
- package/src/lib/server.ts +62 -17
- package/src/lib/storage.ts +39 -0
- package/src/lib/tss.ts +111 -7
- package/test/expressapp.test.ts +99 -0
- package/test/integration/server.test.ts +53 -1
- package/test/integration/tss.test.ts +428 -0
- package/ts_build/package.json +4 -4
- 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 +2 -1
- package/ts_build/src/lib/common/index.d.ts.map +1 -1
- package/ts_build/src/lib/common/utils.d.ts +1 -0
- package/ts_build/src/lib/common/utils.d.ts.map +1 -1
- package/ts_build/src/lib/common/utils.js +16 -1
- package/ts_build/src/lib/common/utils.js.map +1 -1
- package/ts_build/src/lib/model/tsskeygen.d.ts +10 -0
- package/ts_build/src/lib/model/tsskeygen.d.ts.map +1 -1
- package/ts_build/src/lib/model/tsskeygen.js +3 -0
- package/ts_build/src/lib/model/tsskeygen.js.map +1 -1
- package/ts_build/src/lib/routes/maintenance.d.ts.map +1 -1
- package/ts_build/src/lib/routes/maintenance.js +12 -4
- package/ts_build/src/lib/routes/maintenance.js.map +1 -1
- package/ts_build/src/lib/routes/middleware/authTssRequest.d.ts +6 -2
- package/ts_build/src/lib/routes/middleware/authTssRequest.d.ts.map +1 -1
- package/ts_build/src/lib/routes/middleware/authTssRequest.js +21 -14
- package/ts_build/src/lib/routes/middleware/authTssRequest.js.map +1 -1
- package/ts_build/src/lib/routes/middleware/verifyTssMessage.d.ts.map +1 -1
- package/ts_build/src/lib/routes/middleware/verifyTssMessage.js +2 -1
- package/ts_build/src/lib/routes/middleware/verifyTssMessage.js.map +1 -1
- package/ts_build/src/lib/routes/services.d.ts.map +1 -1
- package/ts_build/src/lib/routes/services.js +6 -1
- 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 +32 -30
- package/ts_build/src/lib/routes/tss.js.map +1 -1
- package/ts_build/src/lib/server.d.ts +23 -2
- package/ts_build/src/lib/server.d.ts.map +1 -1
- package/ts_build/src/lib/server.js +20 -3
- package/ts_build/src/lib/server.js.map +1 -1
- package/ts_build/src/lib/storage.d.ts +9 -0
- package/ts_build/src/lib/storage.d.ts.map +1 -1
- package/ts_build/src/lib/storage.js +26 -0
- package/ts_build/src/lib/storage.js.map +1 -1
- package/ts_build/src/lib/tss.d.ts +12 -0
- package/ts_build/src/lib/tss.d.ts.map +1 -1
- package/ts_build/src/lib/tss.js +64 -5
- package/ts_build/src/lib/tss.js.map +1 -1
- package/ts_build/test/expressapp.test.js +90 -0
- package/ts_build/test/expressapp.test.js.map +1 -1
- package/ts_build/test/integration/server.test.js +47 -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 +344 -0
- package/ts_build/test/integration/tss.test.js.map +1 -1
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@bitpay-labs/bitcore-wallet-service",
|
|
3
3
|
"description": "A service for Mutisig HD Bitcoin Wallets",
|
|
4
4
|
"author": "BitPay Inc",
|
|
5
|
-
"version": "11.11.
|
|
5
|
+
"version": "11.11.3",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"main": "ts_build/src/index.js",
|
|
8
8
|
"types": "./ts_build/src/index.d.ts",
|
|
@@ -107,5 +107,5 @@
|
|
|
107
107
|
"email": "kjoseph@bitpay.com"
|
|
108
108
|
}
|
|
109
109
|
],
|
|
110
|
-
"gitHead": "
|
|
110
|
+
"gitHead": "154120b032bb0cd42bad2559c0ef81873a8c7e8c"
|
|
111
111
|
}
|
|
@@ -226,7 +226,7 @@ export const Constants = {
|
|
|
226
226
|
|
|
227
227
|
// May be different from the defaults if we rollback
|
|
228
228
|
// or want to beta test new versions.
|
|
229
|
-
TSS_KEYGEN_SCHEME_VERSION_MAX: 1.
|
|
229
|
+
TSS_KEYGEN_SCHEME_VERSION_MAX: 1.3,
|
|
230
230
|
TSS_SIGGEN_SCHEME_VERSION_MAX: 1.1,
|
|
231
231
|
|
|
232
232
|
TSS_KEYGEN_SCHEME_VERSION_MIN: 1,
|
package/src/lib/common/utils.ts
CHANGED
|
@@ -571,5 +571,22 @@ export const Utils = {
|
|
|
571
571
|
return retval;
|
|
572
572
|
},
|
|
573
573
|
|
|
574
|
-
sleep(ms) {
|
|
574
|
+
sleep(ms) {
|
|
575
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
576
|
+
},
|
|
577
|
+
|
|
578
|
+
convertXPubKeyForNetwork(xPubKey: string, chain: string, network: string) {
|
|
579
|
+
// Convert named network for non-UTXO chains to generic name (e.g. sepolia -> testnet)
|
|
580
|
+
network = !Bitcore_[chain] ? Utils.getGenericName(network) : network;
|
|
581
|
+
const lib = Bitcore_[chain] || BitcoreLib;
|
|
582
|
+
$.checkArgument(xPubKey, 'xPubKey is required at <convertXPubKeyForNetwork()>');
|
|
583
|
+
$.checkArgument(chain, 'chain is required at <convertXPubKeyForNetwork()>');
|
|
584
|
+
$.checkArgument(lib.Networks.get(network), 'invalid network at <convertXPubKeyForNetwork()>: ' + network);
|
|
585
|
+
const hdPubKeyObj = new lib.HDPublicKey(xPubKey).toObject();
|
|
586
|
+
hdPubKeyObj.network = network;
|
|
587
|
+
delete hdPubKeyObj.checksum;
|
|
588
|
+
delete hdPubKeyObj.xpubkey;
|
|
589
|
+
const newHdPubKey = lib.HDPublicKey.fromObject(hdPubKeyObj);
|
|
590
|
+
return newHdPubKey.toString();
|
|
591
|
+
}
|
|
575
592
|
};
|
|
@@ -80,9 +80,18 @@ export interface ITssKeyGenModel {
|
|
|
80
80
|
*/
|
|
81
81
|
timeLimit?: number;
|
|
82
82
|
/**
|
|
83
|
+
* @deprecated Use bwsJoinSecrets instead.
|
|
83
84
|
* The BWC-generated secret for joining a BWS wallet
|
|
84
85
|
*/
|
|
85
86
|
bwsJoinSecret?: string;
|
|
87
|
+
/**
|
|
88
|
+
* The BWC-generated secrets for joining a BWS wallet
|
|
89
|
+
*/
|
|
90
|
+
bwsJoinSecrets?: {
|
|
91
|
+
[chainNetwork: string]: {
|
|
92
|
+
[partyId: number]: string;
|
|
93
|
+
};
|
|
94
|
+
};
|
|
86
95
|
/**
|
|
87
96
|
* The mongo doc version
|
|
88
97
|
*/
|
|
@@ -105,6 +114,11 @@ export class TssKeyGenModel implements ITssKeyGenModel {
|
|
|
105
114
|
createdOn: number;
|
|
106
115
|
timeLimit?: number;
|
|
107
116
|
bwsJoinSecret?: string;
|
|
117
|
+
bwsJoinSecrets?: {
|
|
118
|
+
[chainNetwork: string]: {
|
|
119
|
+
[partyId: number]: string;
|
|
120
|
+
};
|
|
121
|
+
};
|
|
108
122
|
__v: number;
|
|
109
123
|
|
|
110
124
|
|
|
@@ -151,6 +165,7 @@ export class TssKeyGenModel implements ITssKeyGenModel {
|
|
|
151
165
|
x.keyShares = new Array(n);
|
|
152
166
|
x.createdOn = Date.now();
|
|
153
167
|
x.timeLimit = Math.min(timeLimit || Defaults.TSS_KEYGEN_TIME_LIMIT, 60) * ONE_MINUTE; // capped at 60 minutes
|
|
168
|
+
x.bwsJoinSecrets = {};
|
|
154
169
|
x.__v = 0;
|
|
155
170
|
return x;
|
|
156
171
|
}
|
|
@@ -169,6 +184,7 @@ export class TssKeyGenModel implements ITssKeyGenModel {
|
|
|
169
184
|
x.createdOn = obj.createdOn;
|
|
170
185
|
x.timeLimit = obj.timeLimit;
|
|
171
186
|
x.bwsJoinSecret = obj.bwsJoinSecret;
|
|
187
|
+
x.bwsJoinSecrets = obj.bwsJoinSecrets;
|
|
172
188
|
x.__v = obj.__v;
|
|
173
189
|
return x;
|
|
174
190
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import express from 'express';
|
|
2
|
+
import { ClientError } from '../errors/clienterror';
|
|
2
3
|
import { WalletService } from '../server';
|
|
3
4
|
import { Stats } from '../stats';
|
|
4
5
|
import type * as Types from '../../types/expressapp';
|
|
@@ -27,6 +28,14 @@ export function registerMaintenanceRoutes(router: express.Router, context: Route
|
|
|
27
28
|
});
|
|
28
29
|
|
|
29
30
|
router.get('/v1/stats/', (req, res) => {
|
|
31
|
+
const { network, coin } = req.query;
|
|
32
|
+
if (network !== undefined && typeof network !== 'string') {
|
|
33
|
+
return returnError(new ClientError('Invalid network'), res, req);
|
|
34
|
+
}
|
|
35
|
+
if (coin !== undefined && typeof coin !== 'string') {
|
|
36
|
+
return returnError(new ClientError('Invalid coin'), res, req);
|
|
37
|
+
}
|
|
38
|
+
|
|
30
39
|
setPublicCache(res, 1 * ONE_MINUTE);
|
|
31
40
|
const opts: {
|
|
32
41
|
network?: string;
|
|
@@ -35,8 +44,8 @@ export function registerMaintenanceRoutes(router: express.Router, context: Route
|
|
|
35
44
|
to?: string;
|
|
36
45
|
} = {};
|
|
37
46
|
|
|
38
|
-
if (
|
|
39
|
-
if (
|
|
47
|
+
if (network) opts.network = network;
|
|
48
|
+
if (coin) opts.coin = coin;
|
|
40
49
|
if (req.query.from) opts.from = req.query.from as string;
|
|
41
50
|
if (req.query.to) opts.to = req.query.to as string;
|
|
42
51
|
|
|
@@ -11,9 +11,7 @@ import type { ITssSigMessageObject, TssSigGenModel } from '../../model/tsssign';
|
|
|
11
11
|
function verifyMessage(req: express.Request, signature: string, pubKey: string) {
|
|
12
12
|
const message = getMessage(req);
|
|
13
13
|
const isValid = !!Utils.verifyMessage(message, signature, pubKey);
|
|
14
|
-
|
|
15
|
-
throw Errors.NOT_AUTHORIZED.withMessage('Invalid signature');
|
|
16
|
-
}
|
|
14
|
+
return isValid;
|
|
17
15
|
}
|
|
18
16
|
|
|
19
17
|
/**
|
|
@@ -21,7 +19,8 @@ function verifyMessage(req: express.Request, signature: string, pubKey: string)
|
|
|
21
19
|
* It checks if the request has valid credentials, verifies the signature,
|
|
22
20
|
* and retrieves the TSS session based on the request path.
|
|
23
21
|
*/
|
|
24
|
-
export function authTssKeyGenRequest(): express.RequestHandler {
|
|
22
|
+
export function authTssKeyGenRequest(params: { bypassExpired?: boolean } = {}): express.RequestHandler {
|
|
23
|
+
const { bypassExpired } = params;
|
|
25
24
|
return async function(req, res, next) {
|
|
26
25
|
try {
|
|
27
26
|
const storage = WalletService.getStorage();
|
|
@@ -46,7 +45,10 @@ export function authTssKeyGenRequest(): express.RequestHandler {
|
|
|
46
45
|
// This is a brand new session. Caller should be party 0 with the initial round message.
|
|
47
46
|
if (body?.message?.round === 0 && body?.message?.partyId === 0) {
|
|
48
47
|
const pubKey = body?.message?.publicKey;
|
|
49
|
-
verifyMessage(req, signature, pubKey)
|
|
48
|
+
if (!verifyMessage(req, signature, pubKey)) {
|
|
49
|
+
// Self verification, but at least ensures that the caller owns the message's private key
|
|
50
|
+
throw Errors.NOT_AUTHORIZED.withMessage('Invalid signature');
|
|
51
|
+
}
|
|
50
52
|
return next();
|
|
51
53
|
} else {
|
|
52
54
|
// Session should have been initialized by the client already in BWC > TssKeyGen.newKey()
|
|
@@ -54,7 +56,7 @@ export function authTssKeyGenRequest(): express.RequestHandler {
|
|
|
54
56
|
}
|
|
55
57
|
}
|
|
56
58
|
|
|
57
|
-
if (session.isExpired()) {
|
|
59
|
+
if (!bypassExpired && session.isExpired()) {
|
|
58
60
|
throw Errors.TSS_SESSION_EXPIRED;
|
|
59
61
|
}
|
|
60
62
|
|
|
@@ -103,7 +105,9 @@ export function authTssKeyGenRequest(): express.RequestHandler {
|
|
|
103
105
|
throw Errors.NOT_AUTHORIZED.withMessage('Copayer not found in session');
|
|
104
106
|
}
|
|
105
107
|
|
|
106
|
-
verifyMessage(req, signature, pubKey)
|
|
108
|
+
if (!verifyMessage(req, signature, pubKey)) {
|
|
109
|
+
throw Errors.NOT_AUTHORIZED.withMessage('Invalid signature');
|
|
110
|
+
}
|
|
107
111
|
|
|
108
112
|
req.session = session;
|
|
109
113
|
return next();
|
|
@@ -116,7 +120,8 @@ export function authTssKeyGenRequest(): express.RequestHandler {
|
|
|
116
120
|
};
|
|
117
121
|
};
|
|
118
122
|
|
|
119
|
-
export function authTssSignRequest(): express.RequestHandler {
|
|
123
|
+
export function authTssSignRequest(params: { joinable?: boolean } = {}): express.RequestHandler {
|
|
124
|
+
const { joinable } = params;
|
|
120
125
|
return async function(req, res, next) {
|
|
121
126
|
try {
|
|
122
127
|
const storage = WalletService.getStorage();
|
|
@@ -132,16 +137,22 @@ export function authTssSignRequest(): express.RequestHandler {
|
|
|
132
137
|
|
|
133
138
|
const { id } = req.params as { [key: string]: string };
|
|
134
139
|
const body = req.body as { message: ITssSigMessageObject | undefined };
|
|
135
|
-
// Only some requests carry a body.message (e.g. POST /store only sends the final signature).
|
|
136
|
-
// Omitting it is safe since routes that don't receive a message don't consume one.
|
|
137
|
-
const verifyMsg = !!body?.message;
|
|
138
140
|
|
|
139
141
|
const copayer = await storage.fetchCopayerLookup(copayerId);
|
|
140
142
|
if (!copayer) {
|
|
141
143
|
throw Errors.NOT_AUTHORIZED.withMessage('Copayer not found');
|
|
142
144
|
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
+
// I'm not aware of an instance where copayer.requestPubKeys.length > 1, but we iterate over them to find the key for the given signature.
|
|
146
|
+
const pubKey = copayer.requestPubKeys.find(pubKey => verifyMessage(req, signature, pubKey.key))?.key;
|
|
147
|
+
if (!pubKey) {
|
|
148
|
+
throw Errors.NOT_AUTHORIZED.withMessage('Invalid signature');
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Only some requests carry a body.message (e.g. POST /store only sends the final signature).
|
|
152
|
+
// Omitting it is safe since routes that don't receive a message don't change state.
|
|
153
|
+
// `verifyMsgFields` just indicates whether to check the message's fields for expected values (e.g. publicKey, partyId).
|
|
154
|
+
const verifyMsgFields = !!body?.message;
|
|
155
|
+
if (verifyMsgFields && pubKey !== body?.message?.publicKey) {
|
|
145
156
|
throw Errors.NOT_AUTHORIZED.withMessage('Invalid public key');
|
|
146
157
|
}
|
|
147
158
|
|
|
@@ -152,10 +163,12 @@ export function authTssSignRequest(): express.RequestHandler {
|
|
|
152
163
|
throw Errors.TSS_SESSION_EXPIRED;
|
|
153
164
|
}
|
|
154
165
|
const party = session.participants.find(p => p.copayerId === copayerId);
|
|
155
|
-
|
|
166
|
+
// A copayer joins an existing session by sending its round 0 message (participant is added in TssSign.processMessage)
|
|
167
|
+
const isJoining = !party && body?.message?.round === 0;
|
|
168
|
+
if (!party && !(joinable && isJoining)) {
|
|
156
169
|
throw Errors.NOT_AUTHORIZED.withMessage('Copayer not found in session');
|
|
157
170
|
}
|
|
158
|
-
if (
|
|
171
|
+
if (verifyMsgFields && party && party.partyId !== body?.message?.partyId) {
|
|
159
172
|
throw Errors.NOT_AUTHORIZED.withMessage('Copayer not found in session');
|
|
160
173
|
}
|
|
161
174
|
|
|
@@ -164,7 +177,6 @@ export function authTssSignRequest(): express.RequestHandler {
|
|
|
164
177
|
throw Errors.TSS_SESSION_NOT_FOUND;
|
|
165
178
|
}
|
|
166
179
|
|
|
167
|
-
verifyMessage(req, signature, pubKey);
|
|
168
180
|
return next();
|
|
169
181
|
} catch (err) {
|
|
170
182
|
if (err instanceof ClientError) {
|
|
@@ -14,7 +14,10 @@ export async function verifyTssMessage(req, res, next) {
|
|
|
14
14
|
throw Errors.TSS_PUBKEY_MISSING;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
// 1-of-N signing sessions won't have messages for other recipients.
|
|
18
|
+
// req.session is unset for a new session, and keygen sessions have no `m`.
|
|
19
|
+
const isSoloSigner = req.session?.m === 1;
|
|
20
|
+
if (!isSoloSigner && !message.broadcastMessages?.length && !message.p2pMessages?.length) {
|
|
18
21
|
throw Errors.TSS_INVALID_MESSAGE;
|
|
19
22
|
}
|
|
20
23
|
|
|
@@ -3,6 +3,7 @@ import express from 'express';
|
|
|
3
3
|
import rp from 'request-promise-native';
|
|
4
4
|
import config from '../../config';
|
|
5
5
|
import { Common } from '../common';
|
|
6
|
+
import { ClientError } from '../errors/clienterror';
|
|
6
7
|
import type * as Types from '../../types/expressapp';
|
|
7
8
|
import type { WalletService } from '../server';
|
|
8
9
|
|
|
@@ -125,13 +126,18 @@ export function registerServiceRoutes(router: express.Router, context: RouteCont
|
|
|
125
126
|
});
|
|
126
127
|
|
|
127
128
|
router.get('/v1/fiatrates/:code/', (req, res) => {
|
|
129
|
+
const { coin } = req.query;
|
|
130
|
+
if (coin !== undefined && typeof coin !== 'string') {
|
|
131
|
+
return context.returnError(new ClientError('Invalid coin'), res, req);
|
|
132
|
+
}
|
|
133
|
+
|
|
128
134
|
setPublicCache(res, 5 * ONE_MINUTE);
|
|
129
135
|
respondWithPublicServer(req, res, context, server =>
|
|
130
136
|
callbackResult(resolve => {
|
|
131
137
|
server.getFiatRate(
|
|
132
138
|
{
|
|
133
139
|
code: req.params['code'],
|
|
134
|
-
coin:
|
|
140
|
+
coin: coin || 'btc',
|
|
135
141
|
ts: req.query.ts ? +req.query.ts : null
|
|
136
142
|
},
|
|
137
143
|
resolve
|
package/src/lib/routes/tss.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import express from 'express';
|
|
2
2
|
import * as Types from '../../types/expressapp';
|
|
3
3
|
import { TssKeyGen, TssSign } from '../tss';
|
|
4
|
-
import { authRequest } from './middleware/authRequest';
|
|
5
4
|
import { authTssKeyGenRequest, authTssSignRequest } from './middleware/authTssRequest';
|
|
6
5
|
import { createWalletLimiter } from './middleware/createWalletLimiter';
|
|
7
6
|
import { verifyTssMessage } from './middleware/verifyTssMessage';
|
|
@@ -49,6 +48,36 @@ export class TssRouter {
|
|
|
49
48
|
}
|
|
50
49
|
});
|
|
51
50
|
|
|
51
|
+
// Must be before /keygen/:id/:round so "secret" doesn't get interpreted as a round number
|
|
52
|
+
router.get('/v1/tss/keygen/:id/secret', authTssKeyGenRequest({ bypassExpired: true }), async function(req, res) {
|
|
53
|
+
try {
|
|
54
|
+
const id = req.params.id;
|
|
55
|
+
const copayerId = req.headers['x-identity'];
|
|
56
|
+
const { chain, network } = req.query as { [key: string]: string };
|
|
57
|
+
const secret = await TssKeyGen.getBwsJoinSecret({ id, chain, network, copayerId });
|
|
58
|
+
return res.json({ secret });
|
|
59
|
+
} catch (err) {
|
|
60
|
+
return returnError(err ?? 'unknown', res, req);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
router.post('/v1/tss/keygen/:id/secret', authTssKeyGenRequest({ bypassExpired: true }), async function(req, res) {
|
|
65
|
+
try {
|
|
66
|
+
const id = req.params.id;
|
|
67
|
+
const { chain, network, secrets, secret } = req.body;
|
|
68
|
+
const copayerId = req.headers['x-identity'];
|
|
69
|
+
if (secrets == null && secret != null) {
|
|
70
|
+
// Legacy (< v1.3) clients send a single unencrypted secret
|
|
71
|
+
await TssKeyGen.storeBwsJoinSecret({ id, secret, copayerId });
|
|
72
|
+
} else {
|
|
73
|
+
await TssKeyGen.storeBwsJoinSecrets({ id, chain, network, secrets, copayerId });
|
|
74
|
+
}
|
|
75
|
+
return res.send();
|
|
76
|
+
} catch (err) {
|
|
77
|
+
return returnError(err ?? 'unknown', res, req);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
52
81
|
router.get('/v1/tss/keygen/:id/:round', authTssKeyGenRequest(), async function(req, res) {
|
|
53
82
|
let interval: NodeJS.Timeout;
|
|
54
83
|
try {
|
|
@@ -58,10 +87,6 @@ export class TssRouter {
|
|
|
58
87
|
const { id, round } = req.params as { [key: string]: string };
|
|
59
88
|
const { maxWaitTime } = req.query as { [key: string]: string };
|
|
60
89
|
const copayerId = req.headers['x-identity'];
|
|
61
|
-
if (round === 'secret') {
|
|
62
|
-
const secret = await TssKeyGen.getBwsJoinSecret({ id, copayerId });
|
|
63
|
-
return res.json({ secret });
|
|
64
|
-
}
|
|
65
90
|
|
|
66
91
|
// Validate access and fetch session before committing to a streaming response, so that errors like
|
|
67
92
|
// "session not found" or "not a participant" can still be returned with a proper
|
|
@@ -100,33 +125,10 @@ export class TssRouter {
|
|
|
100
125
|
}
|
|
101
126
|
});
|
|
102
127
|
|
|
103
|
-
router.post('/v1/tss/keygen/:id/secret', authTssKeyGenRequest(), async function(req, res) {
|
|
104
|
-
try {
|
|
105
|
-
const id = req.params.id;
|
|
106
|
-
const { secret } = req.body;
|
|
107
|
-
const copayerId = req.headers['x-identity'];
|
|
108
|
-
await TssKeyGen.storeBwsJoinSecret({ id, secret, copayerId });
|
|
109
|
-
return res.send();
|
|
110
|
-
} catch (err) {
|
|
111
|
-
return returnError(err ?? 'unknown', res, req);
|
|
112
|
-
}
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
router.get('/v1/tss/keygen/:id/secret', authTssKeyGenRequest(), async function(req, res) {
|
|
116
|
-
try {
|
|
117
|
-
const id = req.params.id;
|
|
118
|
-
const copayerId = req.headers['x-identity'];
|
|
119
|
-
const secret = await TssKeyGen.getBwsJoinSecret({ id, copayerId });
|
|
120
|
-
return res.json({ secret });
|
|
121
|
-
} catch (err) {
|
|
122
|
-
return returnError(err ?? 'unknown', res, req);
|
|
123
|
-
}
|
|
124
|
-
});
|
|
125
|
-
|
|
126
128
|
|
|
127
129
|
/** Signature Methods */
|
|
128
130
|
|
|
129
|
-
router.post('/v1/tss/sign/:id',
|
|
131
|
+
router.post('/v1/tss/sign/:id', authTssSignRequest({ joinable: true }), verifyTssMessage, async function(req, res) {
|
|
130
132
|
try {
|
|
131
133
|
const id = req.params.id;
|
|
132
134
|
// version was not given by client until 1.1, so fallback to 1.0
|
|
@@ -178,7 +180,8 @@ export class TssRouter {
|
|
|
178
180
|
try {
|
|
179
181
|
const id = req.params.id;
|
|
180
182
|
const { signature } = req.body;
|
|
181
|
-
|
|
183
|
+
const copayerId = req.headers['x-identity'];
|
|
184
|
+
await TssSign.storeSignature({ id, signature, copayerId });
|
|
182
185
|
return res.send();
|
|
183
186
|
} catch (err) {
|
|
184
187
|
return returnError(err ?? 'unknown', res, req);
|
package/src/lib/server.ts
CHANGED
|
@@ -604,6 +604,7 @@ export class WalletService implements IWalletService {
|
|
|
604
604
|
* @param {number} [opts.tssVersion] TSS version to use. Supplying this with n > 1 and a multisig chain (e.g. btc) will tell the wallet to use
|
|
605
605
|
* threshold insead of on-chain multisig. Otherwise, n > 1 and a non-multisig chain will default to TSS_KEYGEN_SCHEME_VERSION
|
|
606
606
|
* @param {string} [opts.tssKeyId] TSS key session id. This is the id of the TSS key generation session that will be used to create the wallet.
|
|
607
|
+
* @param {string} [opts.tssKeygenCopayerId] The ID of the copayer creating the wallet. Required for TSS version >= 1.3.
|
|
607
608
|
*/
|
|
608
609
|
async createWallet(opts, cb) {
|
|
609
610
|
let pubKey;
|
|
@@ -674,6 +675,17 @@ export class WalletService implements IWalletService {
|
|
|
674
675
|
return cb(new ClientError('Invalid TSS version'));
|
|
675
676
|
}
|
|
676
677
|
|
|
678
|
+
if (opts.tssVersion >= 1.3) {
|
|
679
|
+
if (!opts.tssKeygenCopayerId) {
|
|
680
|
+
return cb(new ClientError('Copayer ID is required for TSS version >= 1.3'));
|
|
681
|
+
}
|
|
682
|
+
// This is only a sanity check to prevent problems down the line. If this is somehow
|
|
683
|
+
// spoofed, there are more strict authorization checks later in the wallet creation flow.
|
|
684
|
+
if (keySession.participants.indexOf(opts.tssKeygenCopayerId) !== 0) {
|
|
685
|
+
return cb(new ClientError('Only the TSS session leader can create a wallet'));
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
|
|
677
689
|
// TSS wallets behave like a single-sig
|
|
678
690
|
opts.m = 1;
|
|
679
691
|
opts.n = 1;
|
|
@@ -1038,7 +1050,11 @@ export class WalletService implements IWalletService {
|
|
|
1038
1050
|
this._notify(type, data, {}, cb);
|
|
1039
1051
|
}
|
|
1040
1052
|
|
|
1041
|
-
_addCopayerToWallet(
|
|
1053
|
+
_addCopayerToWallet(
|
|
1054
|
+
wallet: Wallet,
|
|
1055
|
+
opts: JoinWalletOpts,
|
|
1056
|
+
cb: (err: ClientError | null, result?: { copayerId: string | null; wallet: Wallet }) => void
|
|
1057
|
+
) {
|
|
1042
1058
|
const copayer = Copayer.create({
|
|
1043
1059
|
coin: wallet.coin,
|
|
1044
1060
|
chain: wallet.chain, // chain === coin for stored clients
|
|
@@ -1194,20 +1210,11 @@ export class WalletService implements IWalletService {
|
|
|
1194
1210
|
|
|
1195
1211
|
/**
|
|
1196
1212
|
* Joins a wallet in creation.
|
|
1197
|
-
* @param {Object} opts
|
|
1198
|
-
* @param {string} opts.walletId The wallet id.
|
|
1199
|
-
* @param {string} opts.coin The expected coin for this wallet (btc, bch, eth, doge, ltc). Default: btc
|
|
1200
|
-
* @param {string} opts.chain The expected chain for this wallet (btc, bch, eth, doge, ltc). Default: btc
|
|
1201
|
-
* @param {string} opts.name The copayer name.
|
|
1202
|
-
* @param {string} opts.xPubKey Extended Public Key for this copayer
|
|
1203
|
-
* @param {string} opts.hardwareSourcePublicKey Public key from a hardware device for this copayer
|
|
1204
|
-
* @param {string} opts.clientDerivedPublicKey Public key from the client for this wallet
|
|
1205
|
-
* @param {string} opts.requestPubKey Public Key used to check requests from this copayer.
|
|
1206
|
-
* @param {string} opts.copayerSignature S(name|xPubKey|requestPubKey). Used by other copayers to verify that the copayer joining knows the wallet secret.
|
|
1207
|
-
* @param {string} [opts.customData] Custom data for this copayer.
|
|
1208
|
-
* @param {boolean} [opts.dryRun] Simulate the action but do not change server state.
|
|
1209
1213
|
*/
|
|
1210
|
-
joinWallet(
|
|
1214
|
+
joinWallet(
|
|
1215
|
+
opts: JoinWalletOpts,
|
|
1216
|
+
cb: (err: ClientError | null, result?: { copayerId: string | null; wallet: Wallet }) => void
|
|
1217
|
+
) {
|
|
1211
1218
|
// xPubKey is the canonical copayer identity in the current protocol. Alternate public-key
|
|
1212
1219
|
// fields are ancillary metadata; supporting an xPubKey-less copayer requires a versioned
|
|
1213
1220
|
// end-to-end identity protocol rather than a local identity fallback.
|
|
@@ -1226,6 +1233,9 @@ export class WalletService implements IWalletService {
|
|
|
1226
1233
|
opts.chain = opts.coin; // chain === coin for stored clients
|
|
1227
1234
|
}
|
|
1228
1235
|
if (!Utils.checkValueInCollection(opts.chain, Constants.CHAINS)) return cb(new ClientError('Invalid coin'));
|
|
1236
|
+
if (opts.tssKeygenChain != null && !Utils.checkValueInCollection(opts.tssKeygenChain, Constants.CHAINS)) {
|
|
1237
|
+
return cb(new ClientError('Invalid TSS keygen chain'));
|
|
1238
|
+
}
|
|
1229
1239
|
|
|
1230
1240
|
// xPubKey is parsed and validated for every join; ancillary fields never suppress it.
|
|
1231
1241
|
// A future update will re-add conditional logic which allows opts.hardwareSourcePublicKey
|
|
@@ -1286,8 +1296,16 @@ export class WalletService implements IWalletService {
|
|
|
1286
1296
|
|
|
1287
1297
|
if (wallet.tssKeyId) {
|
|
1288
1298
|
const keySession = await storage.fetchTssKeyGenSession({ id: wallet.tssKeyId });
|
|
1289
|
-
|
|
1290
|
-
|
|
1299
|
+
// A sibling wallet on another network (e.g. livenet -> testnet) has an xPubKey serialized for that network,
|
|
1300
|
+
// but the keygen session's copayer IDs were derived from the xPubKey serialized for the session's network.
|
|
1301
|
+
const idChain = opts.tssKeygenChain || opts.chain;
|
|
1302
|
+
const copayerIds = new Set([
|
|
1303
|
+
Copayer.xPubToCopayerId(idChain, opts.xPubKey), // this is probably a duplicate of one of the next two entries, but just in case.
|
|
1304
|
+
Copayer.xPubToCopayerId(idChain, Utils.convertXPubKeyForNetwork(opts.xPubKey, idChain, 'livenet')),
|
|
1305
|
+
Copayer.xPubToCopayerId(idChain, Utils.convertXPubKeyForNetwork(opts.xPubKey, idChain, 'testnet'))
|
|
1306
|
+
]);
|
|
1307
|
+
|
|
1308
|
+
if (!keySession?.participants.some(id => copayerIds.has(id))) {
|
|
1291
1309
|
return cb(Errors.TSS_NON_PARTICIPANT);
|
|
1292
1310
|
}
|
|
1293
1311
|
return this._addCopayerToWallet(wallet, opts, cb);
|
|
@@ -4177,7 +4195,7 @@ export class WalletService implements IWalletService {
|
|
|
4177
4195
|
return cb(err);
|
|
4178
4196
|
}
|
|
4179
4197
|
wallet.beRegistered = true;
|
|
4180
|
-
return this.storage.
|
|
4198
|
+
return this.storage.storeWalletRegistration(wallet, err => cb(err, true));
|
|
4181
4199
|
});
|
|
4182
4200
|
}
|
|
4183
4201
|
|
|
@@ -5838,3 +5856,30 @@ export const UPGRADES = {
|
|
|
5838
5856
|
} as const;
|
|
5839
5857
|
|
|
5840
5858
|
type Upgrade = typeof UPGRADES[keyof typeof UPGRADES];
|
|
5859
|
+
|
|
5860
|
+
export interface JoinWalletOpts {
|
|
5861
|
+
/** The wallet id. */
|
|
5862
|
+
walletId: string;
|
|
5863
|
+
/** The expected coin for this wallet (btc, bch, eth, doge, ltc). Default: btc */
|
|
5864
|
+
coin?: string;
|
|
5865
|
+
/** The expected chain for this wallet (btc, bch, eth, doge, ltc). Default: btc */
|
|
5866
|
+
chain?: string;
|
|
5867
|
+
/** The copayer name. */
|
|
5868
|
+
name: string;
|
|
5869
|
+
/** Extended Public Key for this copayer */
|
|
5870
|
+
xPubKey: string;
|
|
5871
|
+
/** Public key from a hardware device for this copayer */
|
|
5872
|
+
hardwareSourcePublicKey?: string;
|
|
5873
|
+
/** Public key from the client for this wallet */
|
|
5874
|
+
clientDerivedPublicKey?: string;
|
|
5875
|
+
/** Public Key used to check requests from this copayer. */
|
|
5876
|
+
requestPubKey: string;
|
|
5877
|
+
/** Signature proving knowledge of the wallet secret for this copayer. */
|
|
5878
|
+
copayerSignature: string;
|
|
5879
|
+
/** Custom data for this copayer. */
|
|
5880
|
+
customData?: string;
|
|
5881
|
+
/** Simulate the action but do not change server state. */
|
|
5882
|
+
dryRun?: boolean;
|
|
5883
|
+
/** The chain used for TSS key generation. */
|
|
5884
|
+
tssKeygenChain?: string;
|
|
5885
|
+
};
|
package/src/lib/storage.ts
CHANGED
|
@@ -835,6 +835,26 @@ export class Storage {
|
|
|
835
835
|
);
|
|
836
836
|
}
|
|
837
837
|
|
|
838
|
+
storeWalletRegistration(wallet, cb) {
|
|
839
|
+
this.db.collection(collections.WALLETS).updateOne(
|
|
840
|
+
{
|
|
841
|
+
id: wallet.id
|
|
842
|
+
},
|
|
843
|
+
{
|
|
844
|
+
$set: {
|
|
845
|
+
beRegistered: wallet.beRegistered,
|
|
846
|
+
beAuthPrivateKey2: wallet.beAuthPrivateKey2,
|
|
847
|
+
beAuthPublicKey2: wallet.beAuthPublicKey2
|
|
848
|
+
}
|
|
849
|
+
},
|
|
850
|
+
{
|
|
851
|
+
w: 1,
|
|
852
|
+
upsert: false
|
|
853
|
+
},
|
|
854
|
+
cb
|
|
855
|
+
);
|
|
856
|
+
}
|
|
857
|
+
|
|
838
858
|
deregisterWallet(walletId, cb) {
|
|
839
859
|
this.db.collection(collections.WALLETS).updateOne(
|
|
840
860
|
{
|
|
@@ -1961,6 +1981,10 @@ export class Storage {
|
|
|
1961
1981
|
);
|
|
1962
1982
|
}
|
|
1963
1983
|
|
|
1984
|
+
/**
|
|
1985
|
+
* Use only for legacy (< v1.3) clients, where bwsJoinSecrets was not available.
|
|
1986
|
+
* @deprecated Use storeTssKeyBwsJoinSecrets instead.
|
|
1987
|
+
*/
|
|
1964
1988
|
async storeTssKeyBwsJoinSecret({ id, secret }: { id: string; secret: string }) {
|
|
1965
1989
|
return this.db.collection(collections.TSS_KEYGEN).updateOne({
|
|
1966
1990
|
id
|
|
@@ -1973,6 +1997,21 @@ export class Storage {
|
|
|
1973
1997
|
{ upsert: false });
|
|
1974
1998
|
}
|
|
1975
1999
|
|
|
2000
|
+
async storeTssKeyBwsJoinSecrets(params: { id: string; chain: string; network: string; secrets: { [partyId: number]: string } }) {
|
|
2001
|
+
const { id, chain, network, secrets } = params;
|
|
2002
|
+
const _chain = chain.toLowerCase();
|
|
2003
|
+
const _network = network.toLowerCase();
|
|
2004
|
+
return this.db.collection(collections.TSS_KEYGEN).updateOne({
|
|
2005
|
+
id
|
|
2006
|
+
},
|
|
2007
|
+
{
|
|
2008
|
+
$set: {
|
|
2009
|
+
[`bwsJoinSecrets.${_chain}:${_network}`]: secrets
|
|
2010
|
+
}
|
|
2011
|
+
},
|
|
2012
|
+
{ upsert: false });
|
|
2013
|
+
}
|
|
2014
|
+
|
|
1976
2015
|
async fetchTssSigSession({ id }: { id: string }) {
|
|
1977
2016
|
const doc = await this.db.collection(collections.TSS_SIGN).findOne({ id });
|
|
1978
2017
|
if (!doc) {
|