Skip to content

Commit b57fd78

Browse files
committed
feat(ebe): add mpc eddsa signing for ebe
Ticket: WP-4759
1 parent fda31ef commit b57fd78

File tree

7 files changed

+729
-4
lines changed

7 files changed

+729
-4
lines changed
Lines changed: 334 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,334 @@
1+
import 'should';
2+
3+
import * as request from 'supertest';
4+
import nock from 'nock';
5+
import { app as enclavedApp } from '../enclavedApp';
6+
import { AppMode, EnclavedConfig, TlsMode } from '../types';
7+
import express from 'express';
8+
import * as sinon from 'sinon';
9+
import * as configModule from '../initConfig';
10+
import { Ed25519BIP32, Eddsa, SignatureShareType } from '@bitgo/sdk-core';
11+
12+
describe('signMpcTransaction', () => {
13+
let cfg: EnclavedConfig;
14+
let app: express.Application;
15+
let agent: request.SuperAgentTest;
16+
17+
// test config
18+
const kmsUrl = 'http://kms.invalid';
19+
const coin = 'tsol';
20+
const accessToken = 'test-token';
21+
22+
// sinon stubs
23+
let configStub: sinon.SinonStub;
24+
25+
before(() => {
26+
// nock config
27+
nock.disableNetConnect();
28+
nock.enableNetConnect('127.0.0.1');
29+
30+
// app config
31+
cfg = {
32+
appMode: AppMode.ENCLAVED,
33+
port: 0, // Let OS assign a free port
34+
bind: 'localhost',
35+
timeout: 60000,
36+
logFile: '',
37+
kmsUrl: kmsUrl,
38+
tlsMode: TlsMode.DISABLED,
39+
mtlsRequestCert: false,
40+
allowSelfSigned: true,
41+
};
42+
43+
configStub = sinon.stub(configModule, 'initConfig').returns(cfg);
44+
45+
// app setup
46+
app = enclavedApp(cfg);
47+
agent = request.agent(app);
48+
});
49+
50+
afterEach(() => {
51+
nock.cleanAll();
52+
});
53+
54+
after(() => {
55+
configStub.restore();
56+
});
57+
58+
const mockTxRequest = {
59+
apiVersion: 'full',
60+
walletId: '68489ecff6fb16304670b327db8eb31a',
61+
transactions: [
62+
{
63+
unsignedTx: {
64+
derivationPath: 'm/0',
65+
signableHex: 'testMessage',
66+
},
67+
},
68+
],
69+
};
70+
71+
describe('EDDSA MPC Signing Integration Tests', () => {
72+
let hdTree: Ed25519BIP32;
73+
let MPC: Eddsa;
74+
let bitgoGpgPubKey: string;
75+
76+
before(async () => {
77+
hdTree = await Ed25519BIP32.initialize();
78+
MPC = await Eddsa.initialize(hdTree);
79+
bitgoGpgPubKey =
80+
'-----BEGIN PGP PUBLIC KEY BLOCK-----\n' +
81+
'\n' +
82+
'xk8EZo2rshMFK4EEAAoCAwQC6HQa7PXiX2nnpZr/asCcEbgCOcjsR8gcSI8v\n' +
83+
'vMADk59KsFweg+kIzCR3UqfMe2uG6JHwOYpvDREHp/hqtA+hzQViaXRnb8KM\n' +
84+
'BBATCAA+BYJmjauyBAsJBwgJkDwRkYkILA84AxUICgQWAAIBAhkBApsDAh4B\n' +
85+
'FiEEtIZR46psznKbhpKePBGRiQgsDzgAAFehAP4qQ7mRYbDwaBY3Xja36kZQ\n' +
86+
's8vMajrfnesfwXCArF72KQEAoSMkjXtpWWjMbRHMVXFy0EstWqNg7m0FlCGh\n' +
87+
'BsceQZ3OUwRmjauyEgUrgQQACgIDBMHCYxr6G1SaNSiqUpO5BqhZxjQN6355\n' +
88+
'7/p9X36+eKwTKmFFQVecDQrQvIalKc2WoqKxKgCvBSRlOJbBNsxaNN0DAQgH\n' +
89+
'wngEGBMIACoFgmaNq7IJkDwRkYkILA84ApsMFiEEtIZR46psznKbhpKePBGR\n' +
90+
'iQgsDzgAAN/+AQCKM7sRdSRKEkF3vGBSBaqMMAolcK9iujaqkZ/phjNTYwEA\n' +
91+
'mFiLGavuPlAgSCknFZJ0xrrtlLXeWTMjWGU1gsS5Pfo=\n' +
92+
'=7uRX\n' +
93+
'-----END PGP PUBLIC KEY BLOCK-----\n';
94+
});
95+
96+
it('should successfully do all signing rounds with EBE', async () => {
97+
const user = MPC.keyShare(1, 2, 3);
98+
const backup = MPC.keyShare(2, 2, 3);
99+
const bitgo = MPC.keyShare(3, 2, 3);
100+
101+
const userSigningMaterial = {
102+
uShare: user.uShare,
103+
bitgoYShare: bitgo.yShares[1],
104+
backupYShare: backup.yShares[1],
105+
};
106+
107+
const mockKmsResponse = {
108+
prv: JSON.stringify(userSigningMaterial),
109+
pub: 'DSqMPMsMAbEJVNuPKv1ZFdzt6YvJaDPDddfeW7ajtqds',
110+
source: 'user',
111+
type: 'independent',
112+
};
113+
114+
const input = {
115+
source: 'user',
116+
pub: 'DSqMPMsMAbEJVNuPKv1ZFdzt6YvJaDPDddfeW7ajtqds',
117+
txRequest: mockTxRequest,
118+
bitgoGpgPubKey: bitgoGpgPubKey,
119+
};
120+
121+
const mockDataKeyResponse = {
122+
plaintextKey: 'mock-plaintext-data-key',
123+
encryptedKey: 'mock-encrypted-data-key',
124+
};
125+
126+
// Mock KMS responses
127+
const kmsNock = nock(kmsUrl)
128+
.get(`/key/${input.pub}`)
129+
.query({ source: 'user' })
130+
.reply(200, mockKmsResponse);
131+
132+
const dataKeyNock = nock(kmsUrl).post('/generateDataKey').reply(200, mockDataKeyResponse);
133+
134+
const response = await agent
135+
.post(`/api/${coin}/mpc/sign/commitment`)
136+
.set('Authorization', `Bearer ${accessToken}`)
137+
.send(input);
138+
139+
response.status.should.equal(200);
140+
response.body.should.have.property('userToBitgoCommitment');
141+
response.body.should.have.property('encryptedSignerShare');
142+
response.body.should.have.property('encryptedUserToBitgoRShare');
143+
response.body.should.have.property('encryptedDataKey');
144+
145+
kmsNock.done();
146+
dataKeyNock.done();
147+
148+
// Continue with R share test using the returned encryptedUserToBitgoRShare
149+
const encryptedUserToBitgoRShare = response.body.encryptedUserToBitgoRShare;
150+
const encryptedDataKey = response.body.encryptedDataKey;
151+
152+
const rInput = {
153+
source: 'user',
154+
pub: 'DSqMPMsMAbEJVNuPKv1ZFdzt6YvJaDPDddfeW7ajtqds',
155+
txRequest: mockTxRequest,
156+
encryptedUserToBitgoRShare,
157+
encryptedDataKey,
158+
};
159+
160+
const mockDecryptedDataKeyResponse = {
161+
plaintextKey: 'mock-plaintext-data-key',
162+
};
163+
164+
// Mock KMS responses for R share
165+
const rKmsNock = nock(kmsUrl)
166+
.get(`/key/${rInput.pub}`)
167+
.query({ source: 'user' })
168+
.reply(200, mockKmsResponse);
169+
170+
const decryptDataKeyNock = nock(kmsUrl)
171+
.post('/decryptDataKey')
172+
.reply(200, mockDecryptedDataKeyResponse);
173+
174+
const rResponse = await agent
175+
.post(`/api/${coin}/mpc/sign/r`)
176+
.set('Authorization', `Bearer ${accessToken}`)
177+
.send(rInput);
178+
179+
rResponse.status.should.equal(200);
180+
rResponse.body.should.have.property('rShare');
181+
182+
rKmsNock.done();
183+
decryptDataKeyNock.done();
184+
185+
// Continue with G share test using the returned rShare
186+
const rShare = rResponse.body.rShare;
187+
const derivationPath = 'm/0';
188+
const tMessage = 'testMessage';
189+
190+
// Derive signing key and create bitgo sign share
191+
const signingKey = MPC.keyDerive(
192+
userSigningMaterial.uShare,
193+
[userSigningMaterial.bitgoYShare, userSigningMaterial.backupYShare],
194+
derivationPath,
195+
);
196+
197+
const bitgoCombine = MPC.keyCombine(bitgo.uShare, [signingKey.yShares[3], backup.yShares[3]]);
198+
const bitgoSignShare = await MPC.signShare(
199+
Buffer.from(tMessage, 'hex'),
200+
bitgoCombine.pShare,
201+
[bitgoCombine.jShares[1]],
202+
);
203+
204+
const signatureShareRec = {
205+
from: SignatureShareType.BITGO,
206+
to: SignatureShareType.USER,
207+
share: bitgoSignShare.rShares[1].r + bitgoSignShare.rShares[1].R,
208+
};
209+
210+
const bitgoToUserCommitmentShare = {
211+
from: SignatureShareType.BITGO,
212+
to: SignatureShareType.USER,
213+
share: bitgoSignShare.rShares[1].commitment,
214+
type: 'commitment',
215+
};
216+
217+
const gInput = {
218+
source: 'user',
219+
pub: 'DSqMPMsMAbEJVNuPKv1ZFdzt6YvJaDPDddfeW7ajtqds',
220+
txRequest: mockTxRequest,
221+
userToBitgoRShare: rShare,
222+
bitgoToUserRShare: signatureShareRec,
223+
bitgoToUserCommitment: bitgoToUserCommitmentShare,
224+
};
225+
226+
// Mock KMS response for G share
227+
const gKmsNock = nock(kmsUrl)
228+
.get(`/key/${gInput.pub}`)
229+
.query({ source: 'user' })
230+
.reply(200, mockKmsResponse);
231+
232+
const gResponse = await agent
233+
.post(`/api/${coin}/mpc/sign/g`)
234+
.set('Authorization', `Bearer ${accessToken}`)
235+
.send(gInput);
236+
237+
gResponse.status.should.equal(200);
238+
gResponse.body.should.have.property('gShare');
239+
gResponse.body.gShare.should.have.property('i');
240+
gResponse.body.gShare.should.have.property('y');
241+
gResponse.body.gShare.should.have.property('gamma');
242+
gResponse.body.gShare.should.have.property('R');
243+
244+
gKmsNock.done();
245+
});
246+
247+
it('should fail when KMS returns no private key', async () => {
248+
const input = {
249+
source: 'user',
250+
pub: 'DSqMPMsMAbEJVNuPKv1ZFdzt6YvJaDPDddfeW7ajtqds',
251+
txRequest: mockTxRequest,
252+
bitgoGpgPubKey: bitgoGpgPubKey,
253+
};
254+
255+
const kmsNock = nock(kmsUrl)
256+
.get(`/key/${input.pub}`)
257+
.query({ source: 'user' })
258+
.reply(404, { error: 'Key not found' });
259+
260+
const response = await agent
261+
.post(`/api/${coin}/mpc/sign/commitment`)
262+
.set('Authorization', `Bearer ${accessToken}`)
263+
.send(input);
264+
265+
response.status.should.equal(500);
266+
response.body.should.have.property('error');
267+
response.body.should.have.property('details');
268+
response.body.details.should.be.a.String();
269+
response.body.details.should.not.be.empty();
270+
271+
kmsNock.done();
272+
});
273+
274+
it('should fail for unsupported share type', async () => {
275+
const user = MPC.keyShare(1, 2, 3);
276+
const backup = MPC.keyShare(2, 2, 3);
277+
const bitgo = MPC.keyShare(3, 2, 3);
278+
279+
const userSigningMaterial = {
280+
uShare: user.uShare,
281+
bitgoYShare: bitgo.yShares[1],
282+
backupYShare: backup.yShares[1],
283+
};
284+
285+
const mockKmsResponse = {
286+
prv: JSON.stringify(userSigningMaterial),
287+
pub: 'DSqMPMsMAbEJVNuPKv1ZFdzt6YvJaDPDddfeW7ajtqds',
288+
source: 'user',
289+
type: 'independent',
290+
};
291+
292+
const input = {
293+
source: 'user',
294+
pub: 'DSqMPMsMAbEJVNuPKv1ZFdzt6YvJaDPDddfeW7ajtqds',
295+
txRequest: mockTxRequest,
296+
};
297+
298+
const kmsNock = nock(kmsUrl)
299+
.get(`/key/${input.pub}`)
300+
.query({ source: 'user' })
301+
.reply(200, mockKmsResponse);
302+
303+
const response = await agent
304+
.post(`/api/${coin}/mpc/sign/invalid`)
305+
.set('Authorization', `Bearer ${accessToken}`)
306+
.send(input);
307+
308+
response.status.should.equal(500);
309+
response.body.should.have.property('error');
310+
response.body.should.have.property('details');
311+
response.body.details.should.include(
312+
'Share type invalid not supported for EDDSA, only commitment, G and R share generation is supported.',
313+
);
314+
315+
kmsNock.done();
316+
});
317+
318+
it('should fail when required fields are missing', async () => {
319+
const input = {
320+
source: 'user',
321+
pub: 'DSqMPMsMAbEJVNuPKv1ZFdzt6YvJaDPDddfeW7ajtqds',
322+
// Missing txRequest and bitgoGpgPubKey for commitment
323+
};
324+
325+
const response = await agent
326+
.post(`/api/${coin}/mpc/sign/commitment`)
327+
.set('Authorization', `Bearer ${accessToken}`)
328+
.send(input);
329+
330+
response.status.should.equal(500);
331+
response.body.should.have.property('error');
332+
});
333+
});
334+
});

src/api/enclaved/recoveryMultisigTransaction.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,16 @@ import { MethodNotImplementedError } from 'bitgo';
33
import { EnclavedApiSpecRouteRequest } from '../../enclavedBitgoExpress/routers/enclavedApiSpec';
44
import logger from '../../logger';
55
import { isEthLikeCoin } from '../../shared/coinUtils';
6-
import { retrieveKmsKey } from './utils';
6+
import { retrieveKmsPrvKey } from './utils';
77

88
export async function recoveryMultisigTransaction(
99
req: EnclavedApiSpecRouteRequest<'v1.multisig.recovery', 'post'>,
1010
): Promise<any> {
1111
const { userPub, backupPub, unsignedSweepPrebuildTx, walletContractAddress } = req.body;
1212

1313
//fetch prv and check that pub are valid
14-
const userPrv = await retrieveKmsKey({ pub: userPub, source: 'user', cfg: req.config });
15-
const backupPrv = await retrieveKmsKey({ pub: backupPub, source: 'backup', cfg: req.config });
14+
const userPrv = await retrieveKmsPrvKey({ pub: userPub, source: 'user', cfg: req.config });
15+
const backupPrv = await retrieveKmsPrvKey({ pub: backupPub, source: 'backup', cfg: req.config });
1616

1717
if (!userPrv || !backupPrv) {
1818
const errorMsg = `Error while recovery wallet, missing prv keys for user or backup on pub keys user=${userPub}, backup=${backupPub}`;

0 commit comments

Comments
 (0)