Skip to content

Commit f27db52

Browse files
committed
test(bitgo, sdk-api): add e2e v2 signing test for DKLS offline rounds with adata
- e2e 3-round offline signing with v2 encrypted keys and adata context binding - validates v2 envelopes with adata at each round boundary (round 1->2->3) - tests validateAdata rejects mismatched adata on v2 envelopes - fix typesEddsaMPCv2 imports (EddsaMPCv2KeyGen* -> MPCv2KeyGen*) WCN-32
1 parent f9b7a24 commit f27db52

3 files changed

Lines changed: 125 additions & 14 deletions

File tree

modules/bitgo/test/v2/unit/internal/tssUtils/ecdsaMPCv2/signTxRequest.ts

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,122 @@ describe('signTxRequest:', function () {
263263
nockPromises[2].isDone().should.be.true();
264264
});
265265

266+
describe('v2 encryption (offline rounds with adata)', function () {
267+
it('e2e: 3-round offline signing with v2 encrypted keys preserves adata context binding', async function () {
268+
const walletPassphrase = 'testpassphrase';
269+
const userShare = fs.readFileSync(shareFiles[vector.party1]);
270+
const userPrvBase64 = Buffer.from(userShare).toString('base64');
271+
272+
// Encrypt the prv with v2 to trigger the v2 path
273+
const encryptedPrv = await bitgo.encryptAsync({
274+
input: userPrvBase64,
275+
password: walletPassphrase,
276+
encryptionVersion: 2,
277+
});
278+
JSON.parse(encryptedPrv).v.should.equal(2);
279+
280+
// Round 1: encrypt session + GPG key with v2 + adata (purely local, no server call)
281+
const round1Result = await tssUtils.createOfflineRound1Share({
282+
txRequest,
283+
prv: userPrvBase64,
284+
walletPassphrase,
285+
encryptedPrv,
286+
});
287+
288+
// Verify round 1 output has v2 envelopes with adata
289+
const r1SessionEnvelope = JSON.parse(round1Result.encryptedRound1Session);
290+
r1SessionEnvelope.v.should.equal(2);
291+
r1SessionEnvelope.should.have.property('adata');
292+
r1SessionEnvelope.should.have.property('hkdfSalt');
293+
294+
const r1GpgEnvelope = JSON.parse(round1Result.encryptedUserGpgPrvKey);
295+
r1GpgEnvelope.v.should.equal(2);
296+
r1GpgEnvelope.should.have.property('adata');
297+
r1SessionEnvelope.adata.should.equal(r1GpgEnvelope.adata);
298+
299+
// Nock BitGo round 1 response and submit
300+
await nockTxRequestResponseSignatureShareRoundOne(bitgoParty, txRequest, bitgoGpgKey);
301+
const transactions = getRoute('ecdsa');
302+
const round1TxRequestResponse = await bitgo
303+
.post(bitgo.url(`/wallet/${txRequest.walletId}/txrequests/${txRequest.txRequestId + transactions}/sign`, 2))
304+
.send({
305+
signatureShares: [round1Result.signatureShareRound1],
306+
signerGpgPublicKey: round1Result.userGpgPubKey,
307+
})
308+
.result();
309+
310+
// Merge server response with original txRequest (server only returns signatureShares)
311+
const round1TxReq: TxRequest = {
312+
...txRequest,
313+
transactions: [
314+
{
315+
...txRequest.transactions![0],
316+
signatureShares: round1TxRequestResponse.transactions[0].signatureShares,
317+
},
318+
],
319+
};
320+
321+
// Round 2: decrypt v2 round 1 session (validates adata), encrypt round 2 session
322+
const round2Result = await tssUtils.createOfflineRound2Share({
323+
txRequest: round1TxReq,
324+
prv: userPrvBase64,
325+
walletPassphrase,
326+
bitgoPublicGpgKey: bitgoGpgKey.publicKey,
327+
encryptedUserGpgPrvKey: round1Result.encryptedUserGpgPrvKey,
328+
encryptedRound1Session: round1Result.encryptedRound1Session,
329+
});
330+
331+
// Verify round 2 output has v2 envelope with adata
332+
const r2Envelope = JSON.parse(round2Result.encryptedRound2Session);
333+
r2Envelope.v.should.equal(2);
334+
r2Envelope.should.have.property('adata');
335+
r2Envelope.adata.should.equal(r1SessionEnvelope.adata);
336+
337+
// Nock BitGo round 2 response and submit
338+
await nockTxRequestResponseSignatureShareRoundTwo(bitgoParty, txRequest, bitgoGpgKey);
339+
const round2TxRequestResponse = await bitgo
340+
.post(bitgo.url(`/wallet/${txRequest.walletId}/txrequests/${txRequest.txRequestId + transactions}/sign`, 2))
341+
.send({
342+
signatureShares: [round2Result.signatureShareRound2],
343+
signerGpgPublicKey: round1Result.userGpgPubKey,
344+
})
345+
.result();
346+
347+
const round2TxReq: TxRequest = {
348+
...txRequest,
349+
transactions: [
350+
{
351+
...txRequest.transactions![0],
352+
signatureShares: round2TxRequestResponse.transactions[0].signatureShares,
353+
},
354+
],
355+
};
356+
357+
// Round 3: decrypt v2 round 2 session (validates adata), produce final signature share
358+
const round3Result = await tssUtils.createOfflineRound3Share({
359+
txRequest: round2TxReq,
360+
prv: userPrvBase64,
361+
walletPassphrase,
362+
bitgoPublicGpgKey: bitgoGpgKey.publicKey,
363+
encryptedUserGpgPrvKey: round1Result.encryptedUserGpgPrvKey,
364+
encryptedRound2Session: round2Result.encryptedRound2Session,
365+
});
366+
367+
round3Result.should.have.property('signatureShareRound3');
368+
});
369+
370+
it('validateAdata rejects v2 envelopes with mismatched adata', async function () {
371+
const ct = await bitgo.encryptAsync({
372+
input: 'test-data',
373+
password: 'testpass',
374+
encryptionVersion: 2,
375+
adata: 'context-A',
376+
});
377+
378+
(() => (tssUtils as any).validateAdata('context-B', ct)).should.throw(/Adata does not match/);
379+
});
380+
});
381+
266382
it('fails to signs a txRequest for a dkls hot wallet after receiving over 3 429 errors', async function () {
267383
const nockPromises = [
268384
await nockTxRequestResponseSignatureShareRoundOne(bitgoParty, txRequest, bitgoGpgKey),

modules/sdk-api/test/unit/encrypt.ts

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -150,14 +150,6 @@ describe('encryption methods tests', () => {
150150
await assert.rejects(() => decryptV2(password, JSON.stringify(envelope)), /operation-specific reason|incorrect/i);
151151
});
152152

153-
it('encrypts and decrypts without adata (backward compat)', async () => {
154-
const ciphertext = await encryptV2(password, plaintext);
155-
const envelope: V2Envelope = JSON.parse(ciphertext);
156-
assert.strictEqual(envelope.adata, undefined);
157-
const decrypted = await decryptV2(password, ciphertext);
158-
assert.strictEqual(decrypted, plaintext);
159-
});
160-
161153
it('v1 and v2 are independent (v1 data does not decrypt with v2)', async () => {
162154
const v1ct = encrypt(password, plaintext);
163155
await assert.rejects(() => decryptV2(password, v1ct), /invalid envelope/);
Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
11
import * as t from 'io-ts';
22
import {
3-
MPCv2KeyGenRound1Request,
4-
MPCv2KeyGenRound1Response,
5-
MPCv2KeyGenRound2Request,
6-
MPCv2KeyGenRound2Response,
3+
EddsaMPCv2KeyGenRound1Request,
4+
EddsaMPCv2KeyGenRound1Response,
5+
EddsaMPCv2KeyGenRound2Request,
6+
EddsaMPCv2KeyGenRound2Response,
77
} from '@bitgo/public-types';
88

9-
export const generateEddsaMPCv2KeyRequestBody = t.union([MPCv2KeyGenRound1Request, MPCv2KeyGenRound2Request]);
9+
export const generateEddsaMPCv2KeyRequestBody = t.union([EddsaMPCv2KeyGenRound1Request, EddsaMPCv2KeyGenRound2Request]);
1010

1111
export type GenerateEddsaMPCv2KeyRequestBody = t.TypeOf<typeof generateEddsaMPCv2KeyRequestBody>;
1212

13-
export const generateEddsaMPCv2KeyRequestResponse = t.union([MPCv2KeyGenRound1Response, MPCv2KeyGenRound2Response]);
13+
export const generateEddsaMPCv2KeyRequestResponse = t.union([
14+
EddsaMPCv2KeyGenRound1Response,
15+
EddsaMPCv2KeyGenRound2Response,
16+
]);
1417

1518
export type GenerateEddsaMPCv2KeyRequestResponse = t.TypeOf<typeof generateEddsaMPCv2KeyRequestResponse>;

0 commit comments

Comments
 (0)